追記 (2014/05/25)
本記事で、C++ではプリプロセッサ展開後の内容を文字列として扱えないといったが、下記で可能なのでそんなことはない。GoogleTestなどのテスティングフレームワークでテストできる。
#include <iostream>
#define ENUM(a,b) enum{a, b};
#define STRINGIZE(...) STRINGIZE_I(__VA_ARGS__)
#define STRINGIZE_I(...) #__VA_ARGS__
int main() {
std::cout << STRINGIZE(ENUM(value1, value2)) << std::endl; // enum{value1, value2};
return 0;
}
はじめに
C++の進化により、Cプリプロセッサの出番はほとんどなくなったが、どうしても使わないといけない場面がある。そして、それは時として再帰や条件分岐などを含んだ複雑なものとなっている。
これらを手動でテストするのは非常に骨の折れる作業となるので、どうにかして自動化したい。そのためには、どうしたらよいのだろうか?
自動テスト環境の構築
テストを自動化するためには、プリプロセッサ展開後の内容を意図したものと比較できなければならない。そのためには、プリプロセッサ展開後の内容を文字列にする必要がある。さっそく試してみる。
#include <iostream>
#define HOGE hoge
int main() {
std::cout << "HOGE" << std::endl; // hoge
return 0;
}
しかし、このコードの結果は意図したものにならない。
$ g++ -o test test.cpp
$ ./test
HOGE
これは、Cプリプロセッサが文字列リテラル"..."
の中を展開しないためである。つまり、C++では、この手法を用いることはできない。ではどうするのか?
Rubyを使い、プリプロセッサ展開後の内容を文字列にする
前述したとおり、C++ではプリプロセッサ展開後の内容を文字列として扱うことはできない。
しかし、Rubyを使えばこれが可能となる。それは、Rubyが"..."
以外にも文字列リテラルを備えているためである。その文字列リテラルとは以下の2種類である。
%記法
%{文字列}
ヒアドキュメント
<<-STR
文字列
STR
これらを使えば、プリプロセッサが展開されるはずである。
#define HOGE hoge
puts %{HOGE} # hoge
puts <<-STR # hoge
HOGE
STR
しかし、ワーニングが出力され、肝心のRubyのコードは何も出力されない。
$ g++ -E -P test.rb
g++: warning: test.rb: linker input file unused because linking not done
これは、入力ファイルの拡張子がC++以外であったため、処理対象から除外されてしまったからである。これを解決するためには、以下の方法がある。
- 拡張子を
.cpp
などに変える -
-x
オプションを用いて言語を指定
拡張子は変えたくないので、2.
の方法、-x
オプションを付与してみる。
$ g++ -E -P -x c++ test.rb
puts %{hoge} # hoge
puts <<-STR # hoge
hoge
STR
問題なく展開された。実際にこのコードを実行してみるとプリプロセッサ展開後の内容が文字列として扱われているのが確認できる。
$ g++ -E -P -x c++ test.rb | ruby
hoge
hoge
テスティングフレームワークに組み込む
さて、プリプロセッサ展開後の内容を文字列として扱うことに成功したので、テスティングフレームワークに組み込んでみる。テスティングフレームワークは、Ruby標準ライブラリのminitestを用いる。
テストクラスを作る
まず、テストクラスを作成する。minitestはxUnit系なので、Test***
というクラスに、test_***()
というメソッドを作成する。
require 'minitest'
require 'minitest/autorun'
class TestPreprocessor < MiniTest::Test
def test_HOGE
end
end
プリプロセッサの組み込みとアサーションの追加
次に、プリプロセッサとアサーションを追加する。プリプロセッサはテストメソッドの前ならどこでも良いが、今回はテスト対象がわかりやすいように、テストクラス内部に記述した。
テストは、展開後の文字列を期待する文字列と等値比較することによって行う。よって、assert_equal()
アサーションをテストメソッド内に記述する。
require 'minitest'
require 'minitest/autorun'
class TestPreprocessor < MiniTest::Test
#define HOGE hoge
def test_HOGE
assert_equal <<-CPP, 'hoge'
HOGE
CPP
end
end
これでひと通り実装したので、テストを実行してみる。
$ g++ -E -P -x c++ test.rb | ruby - -v
Run options: -v --seed 15787
# Running:
TestPreprocessor#test_HOGE = 0.04 s = F
Finished in 0.040350s, 24.7831 runs/s, 24.7831 assertions/s.
1) Failure:
TestPreprocessor#test_HOGE [-:8]:
--- expected
+++ actual
@@ -1,2 +1 @@
-" hoge
-"
+"hoge"
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
テストに失敗してしまった。どうやら余計な空白が入ってしまっているようだ。
余計な空白を取り除く
上記テストは、期待する文字列に空白を入れれば通るが、毎回これをやるのは面倒くさい。よって、展開後の文字列から余計な空白を取り除くことにする。上記テストクラスに、行頭・行末の空白文字の除去、および連続する空白文字を単一のスペースに置き換えるメソッドformat()
を追加する。
require 'minitest'
require 'minitest/autorun'
class TestPreprocessor < MiniTest::Test
#define HOGE hoge
def format(cpp_code)
cpp_code.gsub(/^\s+/,'').gsub(/\s+$/,'').gsub(/\s+/,' ').strip
end
def test_HOGE
assert_equal format(<<-CPP), 'hoge'
HOGE
CPP
end
end
テストを実行する。
$ g++ -E -P -x c++ test.rb | ruby - -v
test.rb:8:26: warning: empty character constant [-Winvalid-pp-token]
cpp_code.gsub(/^\s+/,'').gsub(/\s+$/,'').gsub(/\s+/,' ').strip
^
test.rb:8:42: warning: empty character constant [-Winvalid-pp-token]
cpp_code.gsub(/^\s+/,'').gsub(/\s+$/,'').gsub(/\s+/,' ').strip
^
2 warnings generated.
Run options: -v --seed 28992
# Running:
TestPreprocessor#test_HOGE = 0.00 s = .
Finished in 0.002473s, 404.3672 runs/s, 404.3672 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
テストは通ったが、不正なプリプロセッサトークンがあるという内容のワーニングが出てしまった。この場合特に問題は無いので、-Wno-invalid-pp-token
オプションを追加して、このワーニングを抑制する。
$ g++ -E -P -Wno-invalid-pp-token -x c++ test.rb | ruby - -v
Run options: -v --seed 7062
# Running:
TestPreprocessor#test_HOGE = 0.00 s = .
Finished in 0.001722s, 580.7201 runs/s, 580.7201 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
テスト実行の自動化
これで、Cプリプロセッサの自動テストを行うことが出来るようになった。しかし、毎回g++ ...
というコマンドを入力するのは面倒臭い。Rakeを用いてタスクにまとめる。
CXX = ENV['CXX'] || 'g++'
task :default => :test
task :test do
sh "#{CXX} -E -P -Wno-invalid-pp-token -x c++ test.rb | ruby - -v"
end
$ rake
g++ -E -P -Wno-invalid-pp-token -x c++ test.rb | ruby - -v
Run options: -v --seed 49784
# Running:
TestPreprocessor#test_HOGE = 0.00 s = .
Finished in 0.001715s, 583.0904 runs/s, 583.0904 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
実際にTDDしてみる
これまでの作業で、Cプリプロセッサの自動テストを実現することが出来た。しかし、上記で用いたプリプロセッサは、ただ文字列置換をするだけの単純なものである。このような単純なプリプロセッサをテストする必要はないだろう。テストが必要となるのは、もっと複雑な場合である。例として、文字列化可能なenum型の作成を見てみる。
文字列化可能なenum型
C++において、識別子を表現する場合、通常enum
を用いる。enum
は整数型で、値を設定しなければ連番で値が設定される。整数型なので、標準出力すると数値が出力される。しかし、識別子であるので数値自体には興味はない。識別子名が出力されてほしいのだ。
#include <iostream>
enum shop_category {
food,
electronics,
hobby
};
int main() {
std::cout << shop_category::electronics << std::endl; // 1
return 0;
}
そこで、各値ごとに対応する名前を保持したテーブルを用意し、<<演算子
をオーバーロードすることにより実現してみる。
#include <iostream>
#include <string>
#include <map>
enum class shop_category {
food,
electronics,
hobby
};
std::ostream & operator <<(std::ostream & out, shop_category value) {
static std::map<shop_category, std::string> const data{
{shop_category::food, "food"},
{shop_category::electronics, "electronics"},
{shop_category::hobby, "hobby"}
};
return out << data.at(value);
}
int main() {
std::cout << shop_category::electronics << std::endl; // electronics
return 0;
}
しかし、毎回対応テーブルを書くのは大変なので、プリプロセッサを使って以下のように記述したい。
#include <iostream>
#include <string>
#include <map>
STRINGIFIABLE_ENUM(shop_category,
food,
electronics,
hobby
)
int main() {
std::cout << shop_category::electronics << std::endl; // electronics
return 0;
}
プリプロセッサの実装
上記プリプロセッサの実装は下記の様になるが、一つ問題点がある。それは、対応テーブルの初期化リストを出力する部分をどうやって実装するかだ。この部分には、型名type
と識別子リストa,b,c
という入力から{type::a,"a"},{type::b,"b"},{type::c,"c"}
という出力を得られなければならない。このためには、a,b,c
それぞれの要素を巡廻しなければならないが、Cプリプロセッサでこれを実現するのは簡単ではない。なぜなら、Cプリプロセッサはマクロの再帰呼び出しが許されていないからである。これを実現するためには何個ものマクロを定義し、擬似的に再帰を表現しなければならない。しかし、幸いにもboostにBoost.Preprocessor
というプリプロセッサメタプログラミングの為のライブラリが用意されており、この中にループや条件分岐を行うマクロ郡が定義されているので、これらを使って実装する。
#define STRINGIFIABLE_ENUM(type, ...) \
enum class type { \
__VA_ARGS__ \
}; \
std::ostream & operator <<(std::ostream & out, type value) { \
static std::map<type, std::string> const data{ \
/* ここはどうやって実装するのか? */ \
}; \
return out << data.at(value); \
}
テストコードの用意
上記で述べた名前テーブルの初期化リストを出力するマクロをSTRINGIFIABLE_ENUM_NAME_LIST
と定義しよう。このマクロは、型名と識別子リストを引数に取る。このマクロのテストコードは下記のようになる。
require 'minitest'
require 'minitest/autorun'
class TestPreprocessor < MiniTest::Test
CPP_CODE = <<-CPP
#include "preprocessor.hpp"
CPP
def format(cpp_code)
cpp_code.gsub(/^\s+/,'').gsub(/\s+$/,'').gsub(/\s+/,' ').strip
end
def test_STRINGIFIABLE_ENUM_NAME_LIST
assert_equal format(<<-SUBJECT_CPP), format(<<-EXPECTED_CPP)
STRINGIFIABLE_ENUM_NAME_LIST(my_enum,
a,
b,
c
)
SUBJECT_CPP
{my_enum::a, "a"},
{my_enum::b, "b"},
{my_enum::c, "c"},
EXPECTED_CPP
end
end
STRINGIFIABLE_ENUM_NAME_LISTの実装
さて、テストコードは用意したので、このテストコードを満たす実装を書いていこう。
まずは、Boost.Preprocessor
のincludeとSTRINGIFIABLE_ENUM_NAME_LIST
の定義を行う。
#include <boost/preprocessor.hpp>
#define STRINGIFIABLE_ENUM(type, ...) \
enum class type { \
__VA_ARGS__ \
}; \
std::ostream & operator <<(std::ostream & out, type value) { \
static std::map<type, std::string> const data{ \
STRINGIFIABLE_ENUM_NAME_LIST(type, __VA_ARGS__) \
}; \
return out << data.at(value); \
}
#define STRINGIFIABLE_ENUM_NAME_LIST(type, ...)
さて、肝心のループであるが、BOOST_PP_REPEAT
というマクロとBOOST_PP_TUPLE_ELEM
というマクロを用いることにより可能である。
BOOST_PP_REPEAT(count, callback, data)
callback(unused, i, data)
BOOST_PP_TUPLE_ELEM(i, data)
-
BOOST_PP_REPEAT
は、count
で指定された回数、callback
を呼び出す。callback
には現在のインデックスi
とdata
が渡される -
BOOST_PP_TUPLE_ELEM
は、data
のi
番目の要素を返す
これを用いると実装は下記のようになる。
…
#define STRINGIFIABLE_ENUM_NAME_LIST(type, ...) \
BOOST_PP_REPEAT( \
BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), \
STRINGIFIABLE_ENUM_NAME_LIST_ELEM, \
(__VA_ARGS__) \
) \
#define STRINGIFIABLE_ENUM_NAME_LIST_ELEM(unused, i, tuple) \
STRINGIFIABLE_ENUM_NAME_LIST_ELEM_IMPL( \
BOOST_PP_TUPLE_ELEM(i, tuple) \
)
#define STRINGIFIABLE_ENUM_NAME_LIST_ELEM_IMPL(value) \
{value, BOOST_PP_STRINGIZE(value)},
ここで、一旦テストを実行してみる。
$ rake
g++ -std=gnu++11 -E -P -Wno-invalid-pp-token -x c++ test.rb | ruby - -v
Run options: -v --seed 23609
# Running:
TestPreprocessor#test_STRINGIFIABLE_ENUM_NAME_LIST = 0.04 s = F
Finished in 0.040121s, 24.9246 runs/s, 24.9246 assertions/s.
1) Failure:
TestPreprocessor#test_STRINGIFIABLE_ENUM_NAME_LIST [-:23]:
--- expected
+++ actual
@@ -1 +1 @@
-"{a, \"a\"}, {b, \"b\"}, {c, \"c\"},"
+"{my_enum::a, \"a\"}, {my_enum::b, \"b\"}, {my_enum::c, \"c\"},"
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
テストに失敗してしまった。型名を追加し忘れたようだ。型を追加してもう一度テストする。
…
#define STRINGIFIABLE_ENUM_NAME_LIST(type, ...) \
BOOST_PP_REPEAT( \
BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), \
STRINGIFIABLE_ENUM_NAME_LIST_ELEM, \
(type, (__VA_ARGS__)) \
) \
#define STRINGIFIABLE_ENUM_NAME_LIST_ELEM(unused, i, tuple) \
STRINGIFIABLE_ENUM_NAME_LIST_ELEM_IMPL( \
BOOST_PP_TUPLE_ELEM(0, tuple), \
BOOST_PP_TUPLE_ELEM(i, BOOST_PP_TUPLE_ELEM(1, tuple)) \
)
#define STRINGIFIABLE_ENUM_NAME_LIST_ELEM_IMPL(type, value) \
{type::value, BOOST_PP_STRINGIZE(value)},
$ rake
g++ -std=gnu++11 -E -P -Wno-invalid-pp-token -x c++ test.rb | ruby - -v
Run options: -v --seed 63983
# Running:
TestPreprocessor#test_STRINGIFIABLE_ENUM_NAME_LIST = 0.00 s = .
Finished in 0.001831s, 546.1496 runs/s, 546.1496 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
テストに通った。これで実装することが出来た。
ほかのコンパイラでのテスト
実装はひとまず完了したが、ほかのコンパイラだとどうだろうか。この実装ではCプリプロセッサの高度な機能を使用しているので、ほかのコンパイラだと動作しない可能性がある。試しにClang
でテストしてみよう。
$ CXX=clang++ rake
clang++ -std=gnu++11 -E -P -Wno-invalid-pp-token -x c++ test.rb | ruby - -v
Run options: -v --seed 64602
# Running:
TestPreprocessor#test_STRINGIFIABLE_ENUM_NAME_LIST = 0.04 s = F
Finished in 0.039372s, 25.3988 runs/s, 25.3988 assertions/s.
1) Failure:
TestPreprocessor#test_STRINGIFIABLE_ENUM_NAME_LIST [-:22]:
--- expected
+++ actual
@@ -1 +1 @@
-"BOOST_PP_REPEAT_1_BOOST_PP_VARIADIC_SIZE(a, b, c)(STRINGIFIABLE_ENUM_NAME_LIST_ELEM, (my_enum, (a, b, c)))"
+"{my_enum::a, \"a\"}, {my_enum::b, \"b\"}, {my_enum::c, \"c\"},"
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
テストが失敗してしまった。どうやら、Clang
では、可変長引数__VA_ARGS__
が利用できる場合に定義されるマクロBOOST_PP_VARIADICS
を自分で定義しないといけないようだ。boost/preprocessor.hpp
をincludeする前で定義する。
#define BOOST_PP_VARIADICS
#include <boost/preprocessor.hpp>
…
再度テストする。
$ CXX=clang++ rake
clang++ -std=gnu++11 -E -P -Wno-invalid-pp-token -x c++ test.rb | ruby - -v
Run options: -v --seed 29401
# Running:
TestPreprocessor#test_STRINGIFIABLE_ENUM_NAME_LIST = 0.00 s = .
Finished in 0.001870s, 534.7594 runs/s, 534.7594 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
無事テストに通った。
実装したプリプロセッサの使用
実装が完了したので、実際に使ってみる。
#include <iostream>
#include <string>
#include <map>
#include "preprocessor.hpp"
STRINGIFIABLE_ENUM(shop_category,
food,
electronics,
hobby
)
int main() {
std::cout << shop_category::food << std::endl; // food
std::cout << shop_category::electronics << std::endl; // electronics
std::cout << shop_category::hobby << std::endl; // hobby
return 0;
}
$ g++ -std=gnu++11 -o main main.cpp
$ ./main
food
electronics
hobby
これで当初の要望どおりのものができた。
さいごに
CプリプロセッサのTDD環境を整えることによって、プリプロセッサのデバッグの煩雑な作業を軽減することが出来た。また、Boost.Preprocessorにより、ループ・条件分岐などを表現することが可能になり、よりプログラマブルに開発することが出来る。
なお、本記事で用いたコードは GitHub にある。