Boost 逆引きリファレンスのサンプルコードをテスト化してみる

この記事は C++ Advent Calendar 2013 の 8 日目の記事になります(あれ、俺 8 日目なのか……)。このネタを思いついたのは、先月に boostjp の ML で見かけた以下の投稿からでした。

Boost 1.55.0 Beta 1 RCが公開されました。
http://lists.boost.org/Archives/boost/2013/10/206792.php
来週頭にはBoost 1.55.0 Beta 1がリリース、今月末のBoost 1.55.0 RCを経て、11月第1週にはBoost 1.55.0のリリースとなるかと思います。
# ちなみにそういったスケジュールは http://www.boost.org/community/ のカレンダーに書いてあります。
1.55.0リリースしてから問題があっても遅いので、今のうちからテストしたりしてもらえると非常に助かります。

https://groups.google.com/forum/#!topic/boostjp/Xub5TE3VCPM

この投稿を見ながら(投稿内容とはちょっとずれていますが)「あぁ、バージョンアップの度に、サンプルコードがきちんと動作をするかどうかもテストした方が良いのか」と思い、「Boost 逆引きリファレンスのサンプルコード、テスト化計画」を密かに始めてみました。

clown/boostjp では、Boost 逆引きリファレンス に掲載されている各種サンプルコードを出来るだけ原型を保ったままテスト化すると言う試みを行っています。例えば、配列 - boostjp の最初のサンプルコード

#include <iostream>
#include <boost/array.hpp>
#include <algorithm>

void disp(int x) { std::cout << x << ' '; }

int main()
{
    boost::array<int, 3> ar = {3, 1, 4};
    std::for_each(ar.begin(), ar.end(), disp);
}

これを以下のようにテスト化します。

#include <boost/test/unit_test.hpp>
#include <boost/array.hpp>
#include <algorithm>
#include <sstream>
#include "ostream_proxy.h"

BOOST_AUTO_TEST_CASE(test_overview) {
    boost::array<int, 3> ar = { 3, 1, 4 };

    std::ostringstream ss;
    std::for_each(ar.begin(), ar.end(), ostream_proxy(ss));
    BOOST_CHECK_EQUAL(ss.str(), "3 1 4 ");
}

「標準出力(std::cout)へ出力」と言うサンプルコードのテスト方法

サンプルコードの特徴として、標準出力(std::cout)に出力して結果を確かめる(掲載する)と言うコードが多くなります。特に、前述した例のように「アルゴリズムで指定した関数の中で std::cout に出力する」と言うサンプルコードの場合、どうやってテスト化すべきか悩んだのですが、最終的には以下のような補助クラスを用いて std::cout の代わりに std::ostringstream に出力させ、最後に出力した文字列で比較すると言う形を取る事にしました。

class ostream_proxy {
public:
    explicit ostream_proxy(std::ostream& stream, char separator = ' ') :
        stream_(stream), separator_(separator) {}

    template <class T>
    void operator()(const T& value) const {
        stream_ << value << separator_;
    }
}

本当はもう少し完成した状態で記事を書く予定だったのですが、予想以上に手間取ってしまったため現時点でひとまず記事として公開します。static assert(コンパイル時にエラーを発生させるサンプルコード)等いくつかはテスト化できそうにないものもありますが、掲載されているサンプルコードはできるだけを網羅してみようかと思います。

Related Pages

Boost.Test (boost/test/included/unit_test.hpp) の使い方

リンク無しでテスト用のソースファイルが複数ある状況でビルドしようとしたら嵌ったお話。

先日、ユニットテストについての講義をする際に「取りあえず Boost.Test を使ってみましょう」と言うことで、リンク(≒ Boost をビルド)しなくても良い boost/test/included/unit_test.hpp を使おうとしたのですが、以下のようなリンクエラーが出力されました(Visual C++ 2012)。

error LNK2005: "public: __thiscall 
boost::unit_test::ut_detail::auto_test_unit_registrar::auto_test_unit_registrar(class 
boost::unit_test::test_unit_generator const &)" (??
0auto_test_unit_registrar@ut_detail@unit_test@boost@@QAE@ABVtest_unit_generator@23@@Z)
は既に gp2-test.obj で定義されています。

…(後略)…

ビルドを通すのにやや苦労しましたが、結論としては「リンク無しで Boost.Test フレームワークを複数のソースファイルが存在する状況で使用する場合」は以下の点を注意する必要があるようです。

  1. #include <boost/test/included/unit_test.hpp> を記述するのは 1 箇所のみ(2 箇所以上でインクルードすると、前述したリンクエラーが発生)。main.cpp 的なファイルを作成して、そこでインクルードするのが良いか。
  2. それ以外のファイル(各テストを記述したファイル)については、リンクしない場合であっても
    #include <boost/test/unit_test.hpp> と記述する。
  3. このままだと Boost.Test 用のライブラリをリンクしようとしてエラーになるので、コンパイル時に BOOST_TEST_NO_LIB または BOOST_ALL_NO_LIB を定義してリンクしないようにする。

ソースファイルの構成例

1. main.cpp

#define BOOST_TEST_MAIN // or #define BOOST_TEST_MODULE test_module_name
#include <boost/test/included/unit_test.hpp>

2. test1.cpp

#include <boost/test/unit_test.hpp>

BOOST_AUTO_TEST_CASE(test_case1) {
    BOOST_CHECK(true);
}

3. test2.cpp(以下、必要なファイル数だけ同様に)

#include <boost/test/unit_test.hpp>

BOOST_AUTO_TEST_CASE(test_case2) {
    BOOST_CHECK(true);
}

コンパイル時に BOOST_TEST_NO_LIB 指定を忘れずに。気を付けないと嵌りますが、リンク有無に関わらずメインファイル以外のインクルード記述は変わらないので、どちらにも移行しやすいと言うのは利点かもしれません。

C++ のテストフレームワークを選ぶポイント

Boost.勉強会 #8 大阪 に参加してきました。発表者、運営者の皆様お疲れ様でした。今回もいろいろと黒魔術的な発表があり「これが変態かー」と感慨深く見ていたのですが、それはそれとして、個人的に気になっていたテーマとして「C++ でのテスト」に関連する発表が 2 件ほどあったので、楽しみにしていました。

C++ のテストフレームワークも数多く公開されており、好みや用途によって選択肢が変わって来るのですが、まぁ今だと選択肢として挙がるのは「Boost.Testgoogletest か」になってくるのかなと思います。どちら(もしくは、上記以外を含めたどれか)を選択すべきか、と言う問いに対しては、残念ながら現状でも決め手となるものに欠けているのですが、自分が利用する上で選択する上でのポイントを少し検討してみます。

手軽に利用できる

発表中でもありましたが、Boost.Test には「ヘッダファイルのインクルードのみで利用可能なテストフレームワーク」と言うものが用意されており、比較的小さなライブラリ等のプログラムをテストする際には便利でよく利用しています(参考:Boost minimal test - Faith and Brave - C++で遊ぼう)。

ビルドしてリンクする位どうって事ないと感じたりもするのですが、特に、Windows (Visual C++) 上で開発している時には /MT、/MD の違いや x86/x64 の違いで予想外にリンクに手こずる事もあり、リンク作業を敬遠しがちになったりもします。そう言った事もあって「ヘッダファイルのインクルードのみで利用可能」と言うのは魅力的に感じる時も多々あり、boost/test/minimal.hpp にはよくお世話になっています。

部分的にテスト可能

C++ Unit Test Frameworks - ACCU を読んでいて思い出したのですが、「部分的にテスト可能かどうか」と言う点も選択肢を検討する上でポイントになったりします。例えば、Microsoft .NET Framework 系?のプログラムのテストフレームワークである NUnit だと下記のように、テストしたい部分のみを選択して実行できるようになっています。

部分的にテストを行える機能があると、ネットワーク通信用のライブラリ等の「時間のかかる」テストも「テーストハーネス内に記述はするが、通常時(何度も実行する時)は実行されないようにしておく」のような選択肢を取る事もできます。

単体テストは速く走る。速く走らないとしたら、それは単体テストではない。他の種類のテストが単体テストの仮面をかぶっていることもよくある。次に当てはまるものは単体テストではない。

  1. データベースとやり取りをする
  2. ネットワークを介した通信をする
  3. ファイルシステムにアクセスする
  4. 実行するために特別な環境設定を必要とする(環境設定ファイルの編集など)

上記に該当するテストが悪いというわけではない。多くの場合において、そのようなテストを書く価値はあり、しばしばテストハーネス内に記述される。しかし単体テストは、そのようなテストと切り分けて、変更を行うたびに高速で実行できるように保ち続けることが重要である。

レガシーコード改善ガイド(p.17)

部分的にテストを行う機能については、ざっと見たところ Boost.Test、googletest ともに可能なようです。

ディフォルトでは,Google Testはユーザが定義したすべてのテストを実行します.しかし,ときにはテストの一部だけを実行したい場合もあるでしょう(たとえばデバッグや変更の速やかな確認のために).GTEST_FILTER環境変数か,--gtest_filterフラグをセットすることで,このフィルタに一致するテストだけを実行することが可能となります(形式は,TestCaseName.TestNameです)

GoogleTestAdvacnedGuide(翻訳)

これをひとまとめにしてテストされるプログラムも参照可能にしてコンパイルすると、すべてのテストを実行してくれるテストプログラムができる。WARNの表示や一部のテストだけを実行したいときは、コマンドラインで引数を指定すればよい。

(例)
    --log_level=warning (エラーレベルがWARNのところも表示)
    --run_test=suite名,suite名/test名 (特定のテストだけ実行)
Boost Testメモ

CppUnit と CppUnitLite

最後に蛇足的な話題。C++ のテストフレームワークと言うと CppUnit もよく名前が挙がるのですが、もう少し簡単に記述できるテストフレームワークとして CppUnitLite と言うものがあるそうです。

最初に CppUnit を開発した時、私はできるだけ JUnit に近いものにしようと考えました。そのほうが xUnit のアーキテクチャを見たことがある人にとって使いやすいだろうと考えたからです。しかしすぐさま、C++ ではきれいに実装することが難しい、あるいは不可能な事柄が次々に出てきました。これは C++Java の機能の違いによるものです。一番の問題は C++ にリフレクションの機能がないことでした。Java では、派生クラスのメソッドに対する参照を持って、実行時にメソッドを検索するといったことが可能です。しかし C++ では、実行時にアクセスすべきメソッドを登録するコードは人間が書いてやる必要があります。その結果、CppUnit は少し使いにくく理解づらいものになりました。

・・・(中略)・・・

CppUnit も CppUnitLite もテストハーネスとして利用可能です。CppUnitLite を使って書いたテストのほうが少し短いため、本書の C++ の例では CppUnitLite を使用しています。

レガシーコード改善ガイド(p.57-59)

Web 上での C++11 関連記事まとめ

これは,C++11 Advent Calendar 2011 参加記事です.現在,開催されている Advent Calendar の一覧は Advent Calendar 2011 (jp) 開催予定リスト - Life like a clown を参照下さい.

C++11 (C++0x) に関しての情報は 本の虫Faith and Brave - C++で遊ぼう でかなり詳細な記述がありますので,情報を探す場合は site:cpplover.blogspot.comsite:d.hatena.ne.jp/faith_and_brave 辺りを指定してググってみると良いかもしれません.この記事でも,該当ブログの記事をメインにリンク集を作成しています.

尚,自分の理解が追い付いてないものや見逃したもの等があるので,結構な抜けがあるかと思います.

型推論・型指定子 (auto, decltype)

auto は右辺値の型を推論してくれる機能,decltype は式の演算結果の型を取得する機能となります.

初期化構文 (initializer_list)

std::vector 等の STL コンテナの初期化が,一つずつ push_back などのようにしなくとも {} (波括弧) で一括して行えるようになる機能です.これに合わせて,全ての初期化が {} (波括弧) で統一できるようにもなるようです.尚,リスト初期化は同じ型しか渡せないという制限があるようで,任意の数の違う型の引数を指定する場合は Variadic Templates を代わりに使う事になるようです.

ラムダ式

匿名関数(無名関数)と呼ばれるもの.昔,一番最初に聞いた時は「は?何それ?」と言う感想を抱いたのですが,他プログラミング言語を見ても随分とポピュラーな機能になってきた感があります.

rvalue reference, および Move Semantics

C++03 での問題点の一つとしてよく挙げられていた「コピーされすぎ(コピーコンストラクタが実行されすぎ)」を緩和する事を目的の一つとして導入された機能です.名称等の問題もあって馴染みにくい機能ですが,第 3 者にライブラリを提供する立場にある開発者であれば,これから必須となって来る機能だろうと思います.

定数式 (constexpr)

コンパイル時に実行される関数を定義できるようになると言うもの.constexpr 指定のものであれば swtich 文の case にも使用できるようになりので,これまでマクロで行われていた部分が代替可能になる可能性があります.ただし,記述する関数本体に厳しい制限があるので,実装するのが大変になる場合があるかもしれません.

ライブラリ

C++11 で新規に導入されるライブラリ群に関するリンク.C++11 で新規に導入されたライブラリの多くは Boost C++ Libraries から採用されたものであるため,Boost C++ Libraries の該当ライブラリの説明も参考になります.また,いくつかのものは TR1 (Technical Report 1) として,コンパイラによっては既に実装されている事もあります.

文字列・数値の相互変換

boost::lexical_cast() を用いて行っていた文字列・数値の相互変換関数群.

日付・時間

C++0x から Boost へ逆輸入された珍しい例.そのため,Boost には Boost.DateTime と Boost.Chrono と言う 2 種類の日付・時間を扱うライブラリが存在しています.

参照ラッパ
メンバ関数アダプタ (mem_fn)
エラー情報のサポート (system_error)

その他

単体の章を設けるかその他のサブ項目にするかについては,完全に私の主観で行ってますので,いろいろあるかとは思いますがまぁ・・・反応を見ながら移した方がいいかなと判断した場合は変更するかもしれません.

NULL ポインタ (nullptr)

NULL が意図せず int 型に推論されないようにするための修正.

final, および override 指定
テンプレートの別名付け (Template Aliases)

template クラスの typedef.実際には typedef ではなく using と言うキーワードを使うようです.

例外を送出しないことの明示 (noexcept)

C++03 までは,デストラクタ等によく throw() と書いていた部分に相当するものです.C++11 で例外指定が deprecated になったので,その代わりと言う感じでしょうか.

コンパイル時 assert (static_assert)

assert() のコンパイル時版.BOOST_STATIC_ASSERT() と言うマクロがありましたが,これが言語機能としてサポートされたと言う感じでしょうか.

関数の戻り値の後置

decltype との併用で便利な場面が出てくると言った感じでしょうか.

コンパイラが生成する関数への default/delete 指定

コンパイラによって自動生成されるコンストラクタ,デストラクタ,代入演算子等の各種関数がある程度制御できるようになります.

明示的な型変換

キャスト演算子とも言われていますが,これに explicit キーワードを付けられるようになるようです.explicit キーワードの付いた型変換用演算子を使用する場合は,(Type)x のように変換後の型を明示的に記述する必要があります.

委譲コンストラクタ

あるコンストラクタから,同じクラスの別のコンストラクタが利用できるようになるようです.

メンバ変数の初期値の指定

C# 辺りで馴染みのあるメンバ変数の定義部分での初期化が可能になるようです.

Boost.GIL で HSV 変換を行う

これは,Boost Advent Calendar 2011 参加記事です.現在,開催されている Advent Calendar の一覧は Advent Calendar 2011 (jp) 開催予定リスト - Life like a clown を参照下さい.

当初,Boost.GIL の(初歩的な)逆引きリファレンス的な記事にしようかと思ったのですが,Boost.勉強会の Boost.GIL 画像処理入門 (PDF) に大体の事が書かれてあってやる事がなかったので,後半に書く予定だった「画像を明るくする」,「画像を鮮やかにする」等の処理を行うための HSV 変換についてをメインに書くことにします.

HSV 変換のアルゴリズムについては以下を参照下さい.

これを Boost.GIL で実現する方法ですが,boost::gil::transform_pixel と言う関数を使うと良いようです.

boost::gil::transform_pixel(const View& src, const View& dst, F functor)

対象のピクセルを受け取って別のピクセルを返す関数を、全ピクセルに対して適用します。この関数は、画素値と出力値が1対1対応である場合に用います(e.g. 明度、コントラスト, レベル補正, トーンカーブ, etc...)。

boost::gil::transform_pixel_positions(const View& src, const View& dst, F functor)

周囲のピクセルの値を元に結果となるピクセルを返す関数を、全ピクセルに対して適用します。transform_pixelとは、受け取る引数がロケータであるという点が異なっており、周囲の画素値を元に実際の出力値を求める場合に有効です(e.g. Blur, Sovel, Laplacian, etc...)。

映像奮闘記: boost.GILで組む、ジェネリックな画像処理のアルゴリズム

尚,第 3 引数に指定する関数オブジェクトは以下のようになります.

class transform_functor {
public:
    template<typename PixelT>
    PixelT operator()(PixelT src);
};

HSV 色空間内で処理を行うための hsvfilter

今回は,この形に沿った関数オブジェクトで「RGB 値を HSV 値に変換してユーザに何らかの処理を行ってもらった後,再び RGB 値に戻す」と言う機能を持つ hsvfilter を実装してみます.尚,現時点では各ピクセルが RGB 値のみを持つ画像でないとエラーになります.それ以外のピクセル型については,そのうち調べて実装していこうかと思います.

#ifndef HSVFILTER_H
#define HSVFILTER_H

#include <boost/tuple/tuple.hpp>
#include <boost/tuple/tuple_io.hpp>
#include <boost/function.hpp>
#include <boost/gil/gil_all.hpp>
#include <cassert>

/* ------------------------------------------------------------------------- */
/*
 *  hsvfilter
 *
 *  各ピクセルを HSV 値に変換した上で何らかの処理を行うためのフィルタ.
 */
/* ------------------------------------------------------------------------- */
class hsvfilter {
public:
    typedef boost::function<void (int&, int&, int&)> predicate_type;
private:
    typedef boost::tuple<int, int, int> rgb_type;
    typedef boost::tuple<int, int, int> hsv_type;
public:
    /* --------------------------------------------------------------------- */
    //  constructor
    /* --------------------------------------------------------------------- */
    explicit hsvfilter(predicate_type pred) : pred_(pred) {}
    
    /* --------------------------------------------------------------------- */
    //  operator()
    /* --------------------------------------------------------------------- */
    template <class PixelT>
    PixelT operator()(PixelT pixel) {
        rgb_type src = this->to_rgb(pixel);
        hsv_type hsv = this->rgb2hsv(src);
        pred_(hsv.get<0>(), hsv.get<1>(), hsv.get<2>());
        rgb_type dest = this->hsv2rgb(hsv);
        return this->from_rgb<PixelT>(dest);
    }

private:
    /* --------------------------------------------------------------------- */
    /*
     *  to_rgb
     *
     *  Boost.GIL の各種ピクセル型から RGB へ変換する.
     *  TODO: 現在は RGB の値のみが格納されてあるものしかサポートされて
     *  いない.その他のピクセル型を調査して対応する.
     */
    /* --------------------------------------------------------------------- */
    template <class PixelT>
    rgb_type to_rgb(const PixelT& pixel) {
        assert(boost::gil::num_channels<PixelT>::value == 3);
        return boost::make_tuple(pixel[0], pixel[1], pixel[2]);
    }
    
    /* --------------------------------------------------------------------- */
    /*
     *  from_rgb
     *
     *  RGB から Boost.GIL の各種ピクセル型へ変換する.
     *  TODO: 現在は RGB の値のみが格納されてあるものしかサポートされて
     *  いない.その他のピクセル型を調査して対応する.
     */
    /* --------------------------------------------------------------------- */
    template <class PixelT>
    PixelT from_rgb(const rgb_type& rgb) {
        assert(boost::gil::num_channels<PixelT>::value == 3);
        
        PixelT dest;
        dest[0] = rgb.get<0>();
        dest[1] = rgb.get<1>();
        dest[2] = rgb.get<2>();
        return dest;
    }
    
    /* --------------------------------------------------------------------- */
    /*
     *  rgb2hsv
     *
     *  RGB 値を HSV 値に変換する.
     */
    /* --------------------------------------------------------------------- */
    hsv_type rgb2hsv(const rgb_type& rgb) {
        const int R = std::min(rgb.get<0>(), 255);
        const int G = std::min(rgb.get<1>(), 255);
        const int B = std::min(rgb.get<2>(), 255);
        
        const int maximum = std::max(std::max(R, G), B);
        const int minimum = std::min(std::min(R, G), B);
        
        const double V = maximum;
        const double S = (maximum != 0) ? 255.0 * (maximum - minimum) / static_cast<double>(maximum) : 0.0;
        
        double H = 0.0;
        if (S > 0.0 && maximum != minimum) {
            if (maximum == R) H = 60.0 * (G - B) / static_cast<double>(maximum - minimum);
            else if (maximum == G) H = 60.0 * (B - R) / static_cast<double>(maximum - minimum) + 120.0;
            else H = 60.0 * (R - G) / static_cast<double>(maximum - minimum) + 240.0;
            if (H < 0.0) H += 360.0;
        }
        
        return boost::make_tuple(static_cast<int>(H), static_cast<int>(S), static_cast<int>(V));
    }
    
    /* --------------------------------------------------------------------- */
    /*
     *  hsv2rgb
     *
     *  HSV 値を RGB 値に変換する.
     */
    /* --------------------------------------------------------------------- */
    rgb_type hsv2rgb(const hsv_type& hsv) {
        const int H = hsv.get<0>();
        const int S = hsv.get<1>();
        const int V = hsv.get<2>();
        
        if (S == 0) return boost::make_tuple(V, V, V);
        
        const int Hi = H / 60;
        const double F = H / 60.0 - Hi;
        const double M = (1.0 - S / 255.0) * V;
        const double N = (1.0 - S / 255.0 * F) * V;
        const double K = (1.0 - S / 255.0 * (1.0 - F)) * V;
        
        switch (Hi) {
            case 0: return boost::make_tuple(V, K, M);
            case 1: return boost::make_tuple(N, V, M);
            case 2: return boost::make_tuple(M, V, K);
            case 3: return boost::make_tuple(M, N, V);
            case 4: return boost::make_tuple(K, M, V);
            case 5: return boost::make_tuple(V, M, N);
            default: break;
        }
        
        return boost::make_tuple(-1, -1, -1);
    }
    
private:
    predicate_type pred_;
};

#endif // HSVFILTER_H

サンプルプログラム

この hsvfilter を使用して,画像を明るくするサンプルプログラムは以下になります.ここでは画像の明るさを調整していますが,これが例えば,画像の鮮やかさを調整する場合は,value の代わりに saturation の値を弄る事になります.

/* ------------------------------------------------------------------------- */
/*
 *  brightener
 *
 *  各ピクセルの明るさを一律に明るくしていくクラス.
 */
/* ------------------------------------------------------------------------- */
class brightener {
public:
    explicit brightener(int brightness) :
        brightness_(brightness) {}
    
    void operator()(int& hue, int& saturation, int& value) {
        value += brightness_;
        if (value > 255) value = 255;
    }
    
private:
    int brightness_;
};

/* ------------------------------------------------------------------------- */
//  main
/* ------------------------------------------------------------------------- */
int main(int argc, char* argv[]) {
    if (argc < 3) return -1;
    
    std::string src_path = argv[1];
    std::string dest_path = argv[2];
    
    boost::gil::rgb8_image_t src_image;
    boost::gil::jpeg_read_image(src_path, src_image);
    boost::gil::rgb8_image_t dest_image(src_image.dimensions());
    
    // メイン処理
    int brightness = 50;
    boost::gil::transform_pixels(
        boost::gil::const_view(src_image),
        boost::gil::view(dest_image),
        hsvfilter(brightener(brightness))
    );
    
    boost::gil::jpeg_write_view(dest_path, boost::gil::view(dest_image));
    
    return 0;
}


右クリックによるドラッグ&ドロップ時のドロップ先のパスを取得する

右クリックでファイルやフォルダをマウスでドラッグ&ドロップすると,「ここにコピー」や「ここに移動」のようなコンテキストメニューが表示されます.

このメニューで「ここに〜」を選択すると,マウスでドロップしたフォルダへ該当の処理(コピー,移動,...)が行われる訳ですが,「ドロップ先のパス」はどうやって取得しているのか分からなかったので調べてみました.

HRESULT Initialize(
  [in]  PCIDLIST_ABSOLUTE pidlFolder,
  [in]  IDataObject *pdtobj,
  [in]  HKEY hkeyProgID
);

・・・(中略)・・・
A pointer to an ITEMIDLIST structure that uniquely identifies a folder. For property sheet extensions, this parameter is NULL. For shortcut menu extensions, it is the item identifier list for the folder that contains the item whose shortcut menu is being displayed. For nondefault drag-and-drop menu extensions, this parameter specifies the target folder.

IShellExtInit::Initialize method (Windows)

右クリックによるドラッグ&ドロップの場合,IShellExtInit::Initialize の引数 pidFolder にドロップ先の PID? が設定されているようなので,この関数を実装する際に PID から該当パスを記憶しておくようです.

STDMETHODIMP Initialize(LPCITEMIDLIST pIDFolder, LPDATAOBJECT pDataObj, HKEY hKeyID) {
    // ...
    
    if(pIDFolder) {
        this->IsDragAndDrop_ = true;
        TCHAR buffer[MAX_PATH] = {};
        ::SHGetPathFromIDList(pIDFolder, buffer);
        this->DropPath_ = buffer;
    }
    else this->IsDragAndDrop_ = false;
    
    // ...
}

_tcscpy_s (strcpy_s, wcscpy_s) の引数

深いディレクトリの削除を再帰的に行う。(ReadOnlyの場合も対応) を使おうとして気づいた事.cstring の文字列処理関数を使おうとすると CRT のセキュリティ機能 関係の警告がよく出てきます.めんどくさいから警告を抑止しようかと思ったのですが,ふと strcpy_s の引数を見ると面白い事に気づきました.

template <size_t size>
errno_t strcpy_s(
   char (&strDestination)[size],
   const char *strSource 
); // C++ only

template <size_t size>
errno_t wcscpy_s(
   wchar_t (&strDestination)[size],
   const wchar_t *strSource 
); // C++ only

template <size_t size>
errno_t _mbscpy_s(
   unsigned char (&strDestination)[size],
   const unsigned char *strSource 
); // C++ only
strcpy_s、wcscpy_s、_mbscpy_s

C++ の場合の strcpy_s 系関数はテンプレートを使用しているものも用意しているようで,第1引数に配列を指定する限りは,ユーザ(呼び出し側)が引数で明示的に要素数を指定しなくても良いようになっているようです.なので,多くの場合は呼び出し側は引数を変更する必要はなく,関数名に _s を付けるだけで良いようになっている模様.

追記: _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES

闇の軍団的には_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES はどうなんだろう? RT @tt_clown: _tcscpy_s (strcpy_s, wcscpy_s) の引数 - http://t.co/BYzdZfng

http://twitter.com/#!/TKinugasa/status/121107078864584704

_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES なんてオプションがあったんですね.

テンプレート オーバーロードを使用することもできます。 _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES を 1 に定義すると、セキュリティが強化されたバリアントを自動的に呼び出す、標準 CRT 関数のテンプレート オーバーロードが有効になります。_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES が 1 の場合、コードを変更する必要はありません。 内部では、strcpy の呼び出しが strcpy_s の呼び出しに変換され、サイズ引数が自動的に指定されます。

#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1

...

char szBuf[10];

strcpy(szBuf, "test"); // ==> strcpy_s(szBuf, 10, "test")

_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES は、strncpy などのカウントを取る関数には影響しません。 カウント関数に対するテンプレート オーバーロードを有効にするには、_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES_COUNT を 1 に定義します。 ただし、定義する前に、コードがバッファー サイズではなく (よくある間違い)、文字数を渡しているかどうかを確認しておく必要があります。 また、セキュリティが強化されたバリアントを呼び出す場合、関数の呼び出し後、バッファーの最後に明示的に null 終端文字を書き込むコードが必要です。 切り捨て動作が必要な場合については、_TRUNCATE の説明を参照してください。

セキュリティ保護されたテンプレート オーバーロード

コンパイラとの互換性を考えると _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES を使うのが一番良いのかなと言う気はします.

GetLastError() に対応するメッセージを取得する

すぐ忘れるのでメモ.GetLastError() でエラー ID を取得して,FormatMessage() でそれに対応するメッセージを取得する関数

#include <string>
#include <windows.h>
#include <tchar.h>

/* ------------------------------------------------------------------------- */
/*
 *  GetErrorMessage
 *
 *  指定されたエラー ID に対応するメッセージを取得する.
 *  http://msdn.microsoft.com/ja-jp/library/cc428939.aspx
 */
/* ------------------------------------------------------------------------- */
std::basic_string<TCHAR> GetErrorMessage(DWORD id) {
    std::basic_string<TCHAR> dest;

    LPVOID buffer = NULL;
    DWORD result = FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
	NULL,
        id,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        reinterpret_cast<TCHAR*>(&buffer),
        0,
        NULL
    );
    	
    if (result > 0 && buffer != NULL) {
        dest = reinterpret_cast<TCHAR*>(buffer);
        LocalFree(buffer);
        
        // 末尾の改行を除去
        if (!dest.empty() && dest[dest.size() - 1] == _T('\n')) dest.erase(dest.size() - 1);
        if (!dest.empty() && dest[dest.size() - 1] == _T('\r')) dest.erase(dest.size() - 1);
    }
    return dest;
}

/* ------------------------------------------------------------------------- */
/*
 *  GetLastErrorMessage
 *
 *  GetLastError() に対応するメッセージを取得する.
 */
/* ------------------------------------------------------------------------- */
std::basic_string<TCHAR> GetLastErrorMessage() {
    return GetErrorMessage(GetLastError());
}

テキストボックスのテキストを取得

テキストボックスのテキストの取得方法のメモ.

#include <iterator>
#include <string>
#include <vector>
#include <windows.h>
#include <tchar.h>

std::basic_string<TCHAR> GetText(HWND handle) {
    int length = ::GetWindowTextLength(handle);
    if (length <= 0) return std::basic_string<TCHAR>();
    
    std::vector<TCHAR> buffer(length + 1, 0);
    int result = ::GetWindowText(handle, reinterpret_cast<TCHAR*>(&buffer[0]), buffer.size());
    if (result <= 0) return std::basic_string<TCHAR>();
    
    // NULL 文字も含まれているなら以下でも良い?
    // return std::basic_string<TCHAR>(reinterpret_cast<TCHAR*>(&buffer[0]))
    std::vector<TCHAR>::iterator pos = buffer.begin();
    std::advance(pos, result);
    return std::basic_string<TCHAR>(buffer.begin(), pos);
}

バッファサイズをどれだけにするかをどうやって決めるんだろうと思っていたら,GetWindowTextLength() で先にテキスト長だけ取得して決めるようです.可変長配列を用意する場合,ややトリッキー?な記述になりますが std::vector を利用するのが最も楽かなと思います.

Trivial logger for VC++

ログ出力をどうしようかと google-glog を触ってみたのですが何か上手くいかず,結局,自作で逃げてしまいました.そんな訳で簡易リファレンス代わり.ログ出力関連は Boost.Log が採択されているようなので,正式に Boost 入りしたらまた考えようと思います.

Examples

使い方としては,Logger::Configuration() で初期設定をした後,各種マクロでログを出力していく形.

#include <windows.h>
#include <tchar.h>
#include <psdotnet/logger.h>
#include <psdotnet/appender.h>

int _tmain(int argc, TCHAR* argv[]) {
#ifndef PSDOTNET_INVALIDATE_LOG_MACRO
    PsdotNet::FileAppender fapp(_T("foo/bar/bas.log"));
    PsdotNet::Logger::Configuration(fapp, PsdotNet::LogLevel::Warn);
#endif
    
    LOG_TRACE(_T("trace macro"));
    LOG_DEBUG(_T("debug macro"));
    LOG_INFO(_T("info macro"));
    LOG_WARN(_T("warn macro"));
    LOG_ERROR(_T("error macro"));
    LOG_FATAL(_T("fatal macro"));
    
    return 0;
}

LogLevel

定義している LogLevel は,Quiet, Trace, Debug, Info, Warn, Error, Fatal の 7 種類.Quiet はまったくログを出力したくない時に指定する定数で,他の値はまぁよく見かけるものを定義しています.Configuration() 時に Info を指定すると LOG_TRACE, LOG_DEBUG のマクロで記述した部分は出力されないとか,そんな感じ.

IAppender

PsdotNet::Logger では IAppender がほとんど仕事をする事になっていて,ここはユーザ定義に任せています.IAppender は,Log4J の Appender と Layout を併せたような役割となります.既に定義してあるのは,指定されたファイルに出力していく FileAppender と,全く何もしない(渡されたログ情報を全て捨てていく) NullAppender の 2 種類になります.

class LogInfo;
class IAppender {
public:
    typedef std::size_t size_type;
    typedef TCHAR char_type;
    typedef std::basic_string<TCHAR> string_type;

    IAppender() {}
    virtual ~IAppender() {}

    virtual void Set(const LogInfo& info) = 0;
    virtual void operator()(const char_type* format, ...) = 0;
    virtual bool Reset() { return true; }
    virtual bool Finish() { return true; }
};

FileAppender のコンストラクタでの第 2 引数に指定できるオプションは現状では以下の通り.オプションは,"|" (OR) で複数指定する事ができます.

  • AppendMode ..... 末尾に追加モードでファイルを開く.
  • FlushOnWrite ..... 書き込む度にフラッシュする.
  • CloseOnWrite ..... 書き込む度にファイルを閉じる.
  • WriteDateTime ..... 日時を出力する.
  • WriteLevel ..... ログレベルを出力する.
  • WriteFileAndLine ..... ファイル名,行番号を出力する.
  • WriteAll ..... 日時,ログレベル,ファイル名,行番号を出力する (WriteDateTime | WriteLevel | WriteFileAndLine と同等).

ログ出力を無効にする方法

ログを出力させないようにする方法は,以下の 3 通りがあります.

  • PsdotNet::Logger::Configuration() を実行しない.
  • PsdotNet::Logger::Configuration() で PsdotNet::LogLevel::Quiet を指定する.
  • psdotnet/logger.h をインクルードする前に PSDOTNET_INVALIDATE_LOG_MACRO を定義する.

このうち上 2 種類に関しては,LOG_XXX() マクロで渡された文字列を NullAppender に渡して捨てる事でログを出力しないようにしています.すなわち,ログ出力はされませんが関数の実行自体は行われています.これに対して,PSDOTNET_INVALIDATE_LOG_MACRO を定義した場合は LOG_XXX() マクロが全てプリプロセス時点で消去されます.このため,関数の実行自体も行われません.