16
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

CプリプロセッサをTDDする

Last updated at Posted at 2014-05-23

追記 (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プリプロセッサの出番はほとんどなくなったが、どうしても使わないといけない場面がある。そして、それは時として再帰や条件分岐などを含んだ複雑なものとなっている。

これらを手動でテストするのは非常に骨の折れる作業となるので、どうにかして自動化したい。そのためには、どうしたらよいのだろうか?

自動テスト環境の構築

テストを自動化するためには、プリプロセッサ展開後の内容を意図したものと比較できなければならない。そのためには、プリプロセッサ展開後の内容を文字列にする必要がある。さっそく試してみる。

test.cpp
#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

これらを使えば、プリプロセッサが展開されるはずである。

test.rb
#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++以外であったため、処理対象から除外されてしまったからである。これを解決するためには、以下の方法がある。

  1. 拡張子を.cppなどに変える
  2. -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_***()というメソッドを作成する。

test.rb
require 'minitest'
require 'minitest/autorun'

class TestPreprocessor < MiniTest::Test
  def test_HOGE
  end
end

プリプロセッサの組み込みとアサーションの追加

次に、プリプロセッサとアサーションを追加する。プリプロセッサはテストメソッドの前ならどこでも良いが、今回はテスト対象がわかりやすいように、テストクラス内部に記述した。

テストは、展開後の文字列を期待する文字列と等値比較することによって行う。よって、assert_equal()アサーションをテストメソッド内に記述する。

test.rb
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()を追加する。

test.rb
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を用いてタスクにまとめる。

Rakefile
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は整数型で、値を設定しなければ連番で値が設定される。整数型なので、標準出力すると数値が出力される。しかし、識別子であるので数値自体には興味はない。識別子名が出力されてほしいのだ。

main.cpp
#include <iostream>

enum shop_category {
    food,
    electronics,
    hobby
};

int main() {
    std::cout << shop_category::electronics << std::endl; // 1
    return 0;
}

そこで、各値ごとに対応する名前を保持したテーブルを用意し、<<演算子をオーバーロードすることにより実現してみる。

main.cpp
#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;
}

しかし、毎回対応テーブルを書くのは大変なので、プリプロセッサを使って以下のように記述したい。

main.cpp
#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というプリプロセッサメタプログラミングの為のライブラリが用意されており、この中にループや条件分岐を行うマクロ郡が定義されているので、これらを使って実装する。

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{ \
            /* ここはどうやって実装するのか? */ \
        }; \
        return out << data.at(value); \
    }

テストコードの用意

上記で述べた名前テーブルの初期化リストを出力するマクロをSTRINGIFIABLE_ENUM_NAME_LISTと定義しよう。このマクロは、型名と識別子リストを引数に取る。このマクロのテストコードは下記のようになる。

test.rb
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の定義を行う。

preprocessor.hpp
#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には現在のインデックスidataが渡される
  • BOOST_PP_TUPLE_ELEMは、datai番目の要素を返す

これを用いると実装は下記のようになる。

preprocessor.hpp


#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

テストに失敗してしまった。型名を追加し忘れたようだ。型を追加してもう一度テストする。

preprocessor.hpp


#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する前で定義する。

preprocessor.hpp
#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

無事テストに通った。

実装したプリプロセッサの使用

実装が完了したので、実際に使ってみる。

main.cpp
#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 にある。

16
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?