Home / プログラミング / boost / wave
wave

C/C++プリプロセッサライブラリ boost::wave の簡単な解説ページです。
boost version 1.55.0 を基にして記述されています。

ライブラリ概要

boost::wave は、構文解析や字句解析を行うライブラリである boost::spirit を用いて実装されたC/C++プリプロセッサライブラリです。

このライブラリは、C/C++ソースコード中の #include#define といったディレクティブを展開することができます。
展開した結果のソースコードはトークン単位のイテレータによって取得することができます。
また、展開処理をフック(コールバック)によって制御することも可能です。

このライブラリは次のような事例に用いることができます。

  • ソースコード文字列のトークン単位での取得。
  • あるソースコードからインクルードされているファイルのリストアップ。

当文書では主要なクラスのメンバ関数に重点を置いて解説します。
それ以外の内容については本家マニュアルを参照してください。

なお、当文書内では名前空間 boost::wavewave と略して記述しています。

基本的な使い方

boost::wave の Hello, World 的なコードは次のようになります。
なお、このコードは後述のサンプルにも入っています。

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
-
!
 
 
 
 
 
 
 
 
-
!
 
-
!
-
|
|
|
|
-
!
-
|
|
|
|
|
!
|
-
!
|
|
|
|
|
|
|
|
-
!
|
|
|
|
|
-
|
!
|
|
|
|
|
-
-
!
|
-
|
!
!
|
-
-
!
|
|
!
|
|
!
// boost::wave 関連ヘッダをインクルード
#include <boost/wave.hpp>
#include <boost/wave/preprocessing_hooks.hpp>
#include <boost/wave/cpplexer/cpp_lex_token.hpp>
#include <boost/wave/cpplexer/cpp_lex_iterator.hpp>
 
#include <iostream>
#include <fstream>
#include <string>
 
// 名前空間のエイリアス名定義
namespace wave = boost::wave;
 
//! メインエントリポイント
int main(int argc, char* argv[])
{
    using namespace std;
 
    if (argc < 2) { return 1; }
 
    // ソースファイルを読み込む
    std::string code;
    {
        std::ifstream fs(argv[1]);
        fs.unsetf(std::ios::skipws);
        code.assign(
            std::istreambuf_iterator<char>(fs.rdbuf()),
            std::istreambuf_iterator<char>());
    }
 
    // コンテキスト用意
    typedef
        wave::context<
            std::string::const_iterator,
            wave::cpplexer::lex_iterator< wave::cpplexer::lex_token<> >,
            wave::iteration_context_policies::load_file_to_string,
            wave::context_policies::default_preprocessing_hooks>
        Context;
    Context ctx(code.begin(), code.end(), argv[1]);
 
    // 言語サポートオプション設定
    ctx.set_language(
        wave::language_support(
            wave::support_cpp               |   // C++として処理
            wave::support_option_long_long  |   // long long 型サポート
            wave::support_option_variadics));   // 可変長引数マクロサポート
 
    // コンパイルオプション設定
    // Visual Studio 2013 がインストールされていることを想定している
    ctx.add_macro_definition("_WIN32");
    ctx.add_macro_definition("_MSC_VER=1800");
    ctx.add_sysinclude_path(
        "C:/Program Files/Microsoft Visual Studio 12.0/VC/include");
 
    try
    {
        // ソースコード解析しつつ結果のトークンを出力
        Context::iterator_type itrEnd = ctx.end();
        for (Context::iterator_type itr = ctx.begin(); itr != itrEnd; ++itr)
        {
            cout << (*itr).get_value();
        }
    }
    catch (const wave::cpp_exception& ex)
    {
        // 例外処理
        cerr << ex.file_name() << " : " << ex.line_no() << endl;
        cerr << "  -> " << ex.description() << endl;
        return 1;
    }
 
    return 0;
}

このソースをコンパイルしてできたプログラムの第一引数に、次のコードが書かれたファイルを渡してみます。

#define RET_CODE 0

// サンプル
int main(int, char**)
{
    return RET_CODE;
}

すると次のように出力されます(前後の改行は省略しています)。
ご覧の通り、 RET_CODE マクロが展開されていることがわかると思います。

int main(int, char**)
{
    return 0;
}

このように、 boost::wave を用いたソースコードの解析は基本的に次の手順で行います。

  1. ソースコード文字列を用意する。
  2. ソースコード文字列とその基となったファイルパスを渡してコンテキストクラス wave::context のインスタンスを作成する。
  3. コンテキストクラスのインスタンスに各種オプションを設定する。
  4. コンテキストクラスのインスタンスから取得したイテレータをインクリメントすることで、コードをトークン単位で解析する。

コンテキストクラス

コンテキストクラスである wave::context について解説します。

クラステンプレート引数

wave::context は次のようにテンプレートで定義されており、5つのテンプレート引数を持ちます。

namespace boost {
namespace wave {

template <
    typename IteratorT,
    typename LexIteratorT, 
    typename InputPolicyT = iteration_context_policies::load_file_to_string,
    typename HooksT = context_policies::eat_whitespace<typename LexIteratorT::token_type>,
    typename DerivedT = this_type
>
class context : private boost::noncopyable
{
    // …略…
};

}   // namespace wave
}   // namespace boost

各テンプレート引数の定義は次の通りです。

引数名定義
IteratorTこのクラスに渡すソースコード文字列のイテレータ型。
通常は std::string::const_iterator を用います。
LexIteratorT字句解析してトークンを取り出すイテレータクラス型。
通常は wave::cpplexer::lex_iterator< wave::cpplexer::lex_token<> > を用います。
InputPolicyT#include によって別のファイルを読み取る際に用いられるクラス型。
普通にファイルを読むならば既定の wave::iteration_context_policies::load_file_to_string を用います。
ただしソースファイルの文字コードがシフトJISである場合はこのクラスをそのまま使うと問題があります。
シフトJIS対応の項を参照してください。
HooksT種々の解析の際に呼び出されるフック関数群を定義したクラス型。
パブリックなコピーコンストラクタを持つクラスである必要があります*1
既定の wave::context_policies::eat_whitespace<...> を用いると余分な空白文字が除去されます。
特に何もさせたくない場合は wave::context_policies::default_preprocessing_hooks を用います。
DerivedTこのクラスを継承したクラスを作成する場合に、継承先のクラス型を指定します(CRTP)。
継承せずに用いるならば既定の wave::this_type を用います。

コンストラクタ

wave::context のコンストラクタは次のように定義されています。

context(
    target_iterator_type const &first_,
    target_iterator_type const &last_,
    char const *fname_ = "<Unknown>",
    HooksT const &hooks_ = HooksT());

各引数の定義は次の通りです。

引数名定義
first_解析するソースコード文字列イテレータの開始位置と終端位置。
ちなみにこれらの値はイテレータ取得時に渡すこともできます(後述)。
その場合、コンストラクタには無意味なイテレータ*2を渡しても問題ありません。
last_
fname_解析対象のファイルパス文字列。
__FILE__ マクロの展開値や解析例外発生時の位置情報に用いられます。
hooks_フッククラスのインスタンス。
フッククラスがパブリックなデフォルトコンストラクタを持つならば省略可能です。
この引数はコピーされる為、コンストラクタへ渡した後に内容を変更してもそれは反映されません。

メンバ関数

主要なメンバ関数のみ解説します。
完全な一覧は本家マニュアルの The Context Object 等を参照してください。

解析用イテレータ取得

iterator_type begin();
iterator_type begin(
    target_iterator_type const &first_,
    target_iterator_type const &last_);
iterator_type end() const;

begin でソースコードのトークン単位解析用イテレータを、 end でその終端を表すイテレータを取得します。

begin には2引数を受け取るオーバロードがあり、こちらを用いる場合は解析するソースコード文字列イテレータの開始位置と終端位置を渡します。
これらはコンストラクタの第1引数および第2引数に渡す値と同様であり、このメンバ関数オーバロードを用いるならばコンストラクタ側には無意味な値を渡しても問題ありません。

戻り値の iterator_typeクラステンプレート引数 LexIteratorT に渡した型となり、トークン情報を保持しています。
トークン情報について詳しくは後述のトークン情報クラスを参照してください。

インクルードパス追加

bool add_include_path(char const *path_);
bool add_sysinclude_path(char const *path_);
void set_sysinclude_delimiter();
void set_current_directory(char const *path_);

#include のパス解決に用いるインクルードパスを追加します。

add_include_path で独自パスを、 add_sysinclude_path でシステム標準パスを追加でき、追加した順に検索されます。
set_sysinclude_delimiter を呼び出した場合、以降 add_include_pathadd_sysinclude_path と同等の処理を行うようになります。

set_current_directory で、相対パスを追加する場合に適用されるカレントディレクトリを設定できます。
カレントディレクトリはパスの追加時に付与されるため、パスを追加した後に set_current_directory を呼び出しても意味はありません。

実際のパスの検索方法は #include <...> の場合と #include "..." の場合とで異なり、次のようになります。

#include <...> の場合
add_sysinclude_path で追加したパスを検索する。
#include "..." の場合
まずインクルード元ファイルと同じ位置を検索し、見つからなければ add_include_path で追加したパスを検索する。

ただし、 set_sysinclude_delimiter を呼び出していた場合は検索方法が変わり、次のようになります。

#include <...> の場合
add_sysinclude_path で追加したパス、および set_sysinclude_delimiter 呼び出し後に add_include_path で追加したパスを検索する。
#include "..." の場合
set_sysinclude_delimiter 呼び出し前に add_include_path で追加したパスを検索する。
インクルード元ファイルと同じ位置は検索されなくなる。

マクロ定義追加

template<typename StringT>
bool add_macro_definition(
    StringT macrostring,
    bool is_predefined = false);

マクロ定義を追加します。

引数 macrostring には単純なキーワード文字列もしくは "=" 区切りで値を指定した文字列を渡します。
例えば "_WIN32" を渡すと #define _WIN32 と同等になり、 "_MSC_VER=1500" を渡すと #define _MSC_VER 1500 と同等になります。

引数 is_predefinedtrue にすると、 __FILE____LINE__ といった組み込みマクロ定義と同等の定義が行えます。
C++標準のマクロ定義を置き換えることもできます。

言語サポートオプション設定

void set_language(
    wave::language_support language_,
    bool reset_macros = true);
wave::language_support get_language() const;

解析処理時に考慮する言語サポートオプションの設定および取得を行います。

set_language の引数 language_ には wave::language_support 列挙値の論理和で設定します。
引数 reset_macros が既定値 true である場合、同時にそれまでに追加したマクロ定義のクリアが行われます。

主要な wave::language_support 列挙値とその定義は次の通りです。

列挙値定義
support_normalC++98の言語仕様をサポートします。
support_cpp
support_option_long_longlong long 型をサポートします。
support_option_variadics可変長引数マクロをサポートします。
support_c99C99の言語仕様をサポートします。
support_option_long_long および support_option_variadics が含まれます。
support_cpp0xC++0x(C++11の原形)の言語仕様をサポートします。
support_option_long_long および support_option_variadics が含まれます。
support_option_insert_whitespace連続する2トークンを連結するとC++キーワード等になる場合、間に空白文字トークンを挿入します。
support_option_preserve_commentsコメント文を除去しないようにします。
support_option_convert_trigraphsTrigraphを有効にします。
詳しくは Trigraph - みねこあ 等を参照してください。
support_option_emit_line_directives出力されるトークン列に #line を挿入し、元の行番号を保持します。
support_option_include_guard_detectionインクルードガードを検出し、検出されたファイルを2回以上開かないようにします。
support_option_emit_pragma_directives不明な #pragma をそのままトークン列として出力するようにします。

フックインスタンス参照取得

hook_policy_type &get_hooks();
hook_policy_type const &get_hooks() const;

フックインスタンスの参照を取得します。
コンストラクタ呼び出し後にフックインスタンスの内容を取得したり変更したりする場合、この関数で取得した参照に対して操作を行います。

トークン情報クラス

クラス概要

コンテキストクラスのクラステンプレート引数 LexIteratorT に指定したイテレータクラスのインスタンスからトークン情報を取得することができます。
通常の wave::cpplexer::lex_iterator< wave::cpplexer::lex_token<> > を用いた場合、トークン情報の型は wave::cpplexer::lex_token<> となります。

トークン情報は解析用イテレータから取得できる他、後述するフッククラスの多くのメンバ関数に TokenT 型の引数として渡されます。

メンバ関数

通常用いられるトークン情報クラスである wave::cpplexer::lex_token<> の主要なメンバ関数を解説します。

トークンID型変換

operator token_id() const;

トークンIDを表す wave::token_id 列挙型への型変換が行えます。

トークンIDはそのトークンがどのようなC++言語要素であるかを表し、本家マニュアルの The Token Identifiers の項にある T_ で始まる値のいずれかになります。
ただし、いくつかマニュアルに記載されていないトークンIDや解説の間違っているトークンIDがあります。
私が気付いたものを次の表にまとめています。

内容トークンID詳細
記載漏れT_EOIEnd Of Input、即ち全入力の終端(=ソースコード文字列イテレータの終端)を表します。
このトークンIDは解析用イテレータからは取得できません。
フッククラスのトークン生成時において wave::T_EOF に続いて渡されます。
記載ミスT_PP_INCLUDE#include "..." と記載されていますが、 #include ... の誤りです。
強制インクルード設定により生成されるトークンです。
T_PP_QHEADER#include <...> と記載されていますが、 #include "..." の誤りです。
T_PP_HHEADER#include ... と記載されていますが、 #include <...> の誤りです。

トークン文字列値取得

string_type const &get_value() const;

トークン文字列値を取得します。

戻り値の string_typeBOOST_WAVE_STRINGTYPE マクロで定義されており、コンパイラによって std::string または boost::wave::util::flex_string<...> となります。
いずれの型であっても出力ストリームにはそのまま渡すことができます。
戻り値を std::string で受け取りたい場合、 boost::wave::util::flex_string<...> をそのまま代入することはできないため、いずれの型であっても定義されているメンバ関数 c_str を用いて次のように代入します。

// トークン情報 token からトークン文字列値を std::string に取得
std::string value = token.get_value().c_str();

トークン位置情報取得

position_type const &get_position() const;

トークンのファイル内における位置情報を取得します。

戻り値の position_type はデフォルトでは wave::util::file_position_type となり、行番号を取得する get_line や行内位置を取得する get_column などのメンバ関数が使えます。
詳しくは本家マニュアルの The File Position の項を参照してください。

EOI(End Of Input; 入力の終端)判定

bool is_eoi() const;

トークンが全入力の終端(=ソースコード文字列イテレータの終端)を表すものであるか否かを取得します。
トークンIDが wave::T_EOI であるか否か調べることと同じです。

トークン有効判定

bool is_valid() const;

トークンが有効な値を保持しているか否かを取得します。

この関数が false を返す場合、 get_valueget_position の呼び出しを行ってはいけません。
呼び出した場合、NULL参照になる可能性があります。

フッククラス

クラス概要

コンテキストクラスのクラステンプレート引数 HooksT に独自のフッククラス型を指定し、コンストラクタの第4引数にそのクラスのインスタンスを渡す*3ことで、ソースコード解析中に次のようなタイミングで処理内容を捕捉することができます。

  • 各種ディレクティブ(#include#define 等)発見時。
  • #include で指定されたファイルの解析開始時および完了時。
  • マクロ展開時。
  • トークン生成時。
  • 解析例外発生時。

また、捕捉種別によっては処理内容の操作を行うことも可能です(#include を無視する等)。

独自のフッククラス型を定義する場合、通常は wave::context_policies::default_preprocessing_hooks を継承して作成します。
wave::context_policies::default_preprocessing_hooks は特別なことを行わないフッククラスであり、このクラスを継承することで不要な捕捉処理を記述する必要が無くなります。

あらゆるディレクティブを処理しないようにするフッククラスのコーディング例は次のようになります。
なお、このコードは後述のサンプルにも入っています。

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
 
-
!
 
 
-
!
 
 
-
|
-
!
|
-
-
!
!
!
#pragma once
 
// boost::wave 関連ヘッダをインクルード
#include <boost/wave.hpp>
#include <boost/wave/preprocessing_hooks.hpp>
 
/// すべてのディレクティブを処理しないようにするフッククラス
class IgnoreDirectiveHook
    :
    public boost::wave::context_policies::default_preprocessing_hooks
{
public:
    /// ディレクティブ発見時の捕捉関数
    template<typename ContextT, typename TokenT>
    bool found_directive(ContextT const&, TokenT const&)
    {
        // true を返すと処理がスキップされる
        return true;
    }
};

メンバ関数テンプレート引数

後述するフッククラスのメンバ関数はテンプレートで定義されており、いくつかのテンプレート引数型を関数の引数として受け取ります。
主要なテンプレート引数の定義は次の通りです。

引数名定義
ContextTコンテキストクラス型。
TokenTトークン情報クラス型。
通常は wave::cpplexer::lex_token<> です。
ContainerTトークン列を表す、トークン情報クラスのリストを保持するSTLコンテナ型。
std::list<TokenT> 相当の型になります。

メンバ関数

フック処理で呼び出されるメンバ関数の一覧です。
すべてのメンバ関数の第1引数には処理対象であるコンテキストクラス ContextT のインスタンス参照が渡されます。

各種ディレクティブ発見時

template<
    typename ContextT,
    typename TokenT
>
bool found_directive(
    ContextT const& ctx,
    TokenT const& directive);
template<typename ContextT>
bool found_include_directive(
    ContextT const& ctx,
    std::string const& filename, 
    bool include_next);
template<
    typename ContextT,
    typename ContainerT
>
bool found_warning_directive(
    ContextT const& ctx,
    ContainerT const& message);
template<
    typename ContextT,
    typename ContainerT
>
bool found_error_directive(
    ContextT const& ctx,
    ContainerT const& message);
template<
    typename ContextT,
    typename ContainerT
>
void found_line_directive(
    ContextT const& ctx,
    ContainerT const& arguments,
    unsigned int line,
    std::string const& filename);

found_directive は、あらゆるディレクティブの発見時に最初に呼び出されます。
引数 directive に発見したディレクティブのトークン情報が渡されます。

found_include_directive#include の発見時に呼び出されます。
引数 filename#include 直後のファイル名指定が "foo.h"<cstdio> といった形の文字列で渡されます。
引数 include_next は、 #include ではなく boost::wave 独自ディレクティブ #include_next が使われた場合に true となります。

found_warning_directive#warning の発見時、 found_error_directive#error の発見時に呼び出されます(#warning は非標準のディレクティブであり、 Visual C++ 等では使えません。)。
引数 message に警告/エラーメッセージ指定のトークン列が渡されます。

found_line_directive#line の発見時に呼び出されます。
引数 arguments に行番号指定およびファイル名指定(省略可能)のトークン列が渡されます。
引数 line および filename には arguments を解釈した結果の値が渡されます。
ファイル名指定を省略した場合、 filename には空の文字列が渡されます。

found_line_directive 以外は、 false を返すと発見したディレクティブの解析処理が継続されます。
true を返すと、発見したディレクティブの解析処理は中止され、ディレクティブが記述されていなかったものとして扱われます(即ち空行扱いになります)。
wave::context_policies::default_preprocessing_hooks の既定の動作ではいずれの関数も false を返します。

なお、 #warning および #error の解析処理を継続させると、続けて例外送出処理が呼び出されます。

条件分岐ディレクティブ判定時

template<
    typename ContextT,
    typename TokenT,
    typename ContainerT
>
bool evaluated_conditional_expression(
    ContextT const& ctx, 
    TokenT const& directive,
    ContainerT const& expression, 
    bool expression_value);

次のいずれかの条件分岐ディレクティブが判定された時に呼び出されます。

  • #if
  • #ifdef
  • #ifndef
  • #elif

引数 directive には上記のいずれかのディレクティブのトークン情報が渡されます。
引数 expression にはディレクティブ直後の条件式のトークン列が渡されます。
引数 expression_value には条件式の判定結果を表す真偽値が渡されます。

この関数が false を返すと判定結果に基づいて以降のコードを展開もしくはスキップします。
true を返すと条件式の判定が再度行われ、再びこの関数が呼び出されます。
wave::context_policies::default_preprocessing_hooks の既定の動作では false を返します。

トークンスキップ時

template<
    typename ContextT,
    typename TokenT
>
void skipped_token(
    ContextT const& ctx,
    TokenT const& token);

#if#else などの条件分岐ディレクティブによってトークンがスキップされた時に呼び出されます。
引数 token にスキップされたトークン情報が渡されます。

トークン生成時

template<
    typename ContextT,
    typename TokenT
>
TokenT const& generated_token(
    ContextT const& ctx,
    TokenT const& token);

解析用イテレータの取得やインクリメントによって新しいトークン情報が生成される際に呼び出されます。
引数 token に生成されたトークン情報が渡されます。

戻り値には実際にイテレータから取得されるトークン情報の参照を返します。
wave::context_policies::default_preprocessing_hooks の既定の動作では token をそのまま返します。

なお、この関数は解析用イテレータが終端に到達した際にも呼び出されます。
その際の引数 token には、 (token.is_eoi() == true) かつ (token.is_valid() == false) であるトークン情報(EOIトークン)が渡されます。
このとき token.get_value()token.get_position() を呼び出そうとするとNULL参照でプログラムが強制終了してしまうため、必ず token.is_valid() で有効なトークン情報であるか否か確かめてからそれらのメンバ関数を呼び出すようにしてください。

マクロ定義時

template<
    typename ContextT,
    typename TokenT,
    typename ParametersT, 
    typename DefinitionT
>
void defined_macro(
    ContextT const& ctx,
    TokenT const& macro_name, 
    bool is_functionlike,
    ParametersT const& parameters, 
    DefinitionT const& definition,
    bool is_predefined);

#define によってマクロが定義された際に呼び出されます。
#define XYZ 100 のような文字列置換マクロと、 #define MUL(x,y) ((x)*(y)) のような引数リスト付きマクロの両方で呼び出されます。

引数 macro_name には定義されたマクロ名のトークン情報が渡されます。
引数リスト付きマクロの場合、例えば MUL(x,y) ならば MUL のトークン情報が渡されます。

引数 is_functionlike には、文字列置換マクロであれば false 、引数リスト付きマクロであれば true が渡されます。

引数 parameters には、引数リスト付きマクロの場合に各引数のトークン情報が配列で渡されます。
文字列置換マクロの場合は空の配列が渡されます。
引数の型 ParametersTstd::vector<TokenT> 相当の型です。

引数 definition には置換内容のトークン列が渡されます。
#define XYZ 100 ならば 100#define MUL(x,y) ((x)*(y)) ならば ((x)*(y)) の部分になります。
引数の型 DefinitionTstd::list<TokenT> 相当の型です。

引数 is_predefined には、マクロが __FILE____LINE__ といった組み込みマクロ定義であれば true が渡されます。

マクロ削除時

template<
    typename ContextT,
    typename TokenT
>
void undefined_macro(
    ContextT const& ctx,
    TokenT const& macro_name);

#undef によってマクロが削除された際に呼び出されます。
引数 macro_name に削除されたマクロ名のトークン情報が渡されます。

マクロ展開開始時

template<
    typename ContextT,
    typename TokenT,
    typename ContainerT
>
bool expanding_object_like_macro(
    ContextT const& ctx,
    TokenT const& macro_name, 
    ContainerT const& definition,
    TokenT const& macrocall);
template<
    typename ContextT,
    typename TokenT,
    typename ContainerT,
    typename IteratorT
>
bool expanding_function_like_macro(
    ContextT const& ctx,
    TokenT const& macro_name,
    std::vector<TokenT> const& parameters, 
    ContainerT const& definition,
    TokenT const& macrocall,
    std::vector<ContainerT> const& arguments,
    IteratorT const& seqstart,
    IteratorT const& seqend);

expanding_object_like_macro は文字列置換マクロの展開開始時、 expanding_function_like_macro は引数リスト付きマクロの展開開始時に呼び出されます。

引数 macro_nameparametersdefinition には展開対象マクロのマクロ定義時と同じ内容が渡されます。
各々のトークン情報の位置情報もマクロ定義時のものになります。

引数 macrocall にはマクロ呼び出し位置におけるマクロ名のトークン情報が渡されます。

引数 arguments には、引数リスト付きマクロの各引数に渡されたトークン列の std::vector 配列が渡されます。
例えば MUL(x,y) というマクロを MUL(100,1+2) のように呼び出した場合、トークン列 100 とトークン列 1+2std::vector 配列の要素になります。

引数 seqstart および seqend は、引数リスト付きマクロ呼び出しにおける引数リスト部分のトークンイテレータが渡されます。
例えば MUL(100,1+2) という呼び出しならば、 seqstart には最初の ( を指すイテレータ、 seqend には最後の ) を指すイテレータがそれぞれ渡されます。

これらの関数が false を返すとマクロの展開が継続されます。
true を返すとマクロの展開が中止され、その時点でのコード文字列がそのまま出力されるトークン列となります。
wave::context_policies::default_preprocessing_hooks の既定の動作では false を返します。

これらの関数は、マクロが入れ子になっている場合は各々の展開時に呼び出されます。
例えば ABC というマクロを展開すると XYZ というマクロになる場合、 ABC の展開時と XYZ の展開時にそれぞれ呼び出されます。
また、それぞれの呼び出しの間にはマクロ展開完了時の呼び出しが挟まります。
詳細な呼び出し順序についてはマクロ展開完了時の項を参照してください。

マクロ展開完了時

template<
    typename ContextT,
    typename ContainerT
>
void expanded_macro(
    ContextT const& ctx,
    ContainerT const& result);
template<
    typename ContextT,
    typename ContainerT
>
void rescanned_macro(
    ContextT const& ctx,
    ContainerT const& result);

expanded_macro はマクロの展開完了時、 rescanned_macro は入れ子になっているマクロも含めた完全な展開完了時に呼び出されます。
引数 result には展開されたトークン列が渡されます。

これらの関数およびマクロ展開開始時に呼び出される関数の呼び出し順序について説明します。
ここでは説明のために次のマクロ定義がされているものとします。

#define ABC (X+Y)
#define X 100
#define Y 50
#define MUL(x,y) ((x)*(y))

このとき、 ABC および MUL(MUL(1,2),3) の展開処理における各関数の呼び出し順序は次のようになります。

ABC の展開時
順序呼び出し関数マクロ展開状態備考
1expanding_object_like_macroABCABC に対する呼び出し。
2expanded_macro(X+Y)
3expanding_object_like_macro(X+Y)X に対する呼び出し。
4expanded_macro(100+Y)
5rescanned_macro(100+Y)
6expanding_object_like_macro(100+Y)Y に対する呼び出し。
7expanded_macro(100+50)
8rescanned_macro(100+50)
9rescanned_macro(100+50)ABC に対する呼び出し。
MUL(MUL(1,2),3) の展開時
順序呼び出し関数マクロ展開状態備考
1expanding_function_like_macroMUL(MUL(1,2),3)MUL(MUL(1,2),3) に対する呼び出し。
2expanding_function_like_macroMUL(MUL(1,2),3)MUL(1,2) に対する呼び出し。
3expanded_macroMUL(((1)*(2)),3)
4rescanned_macroMUL(((1)*(2)),3)
5expanded_macro(((1)*(2)))*(3))MUL(MUL(1,2),3) に対する呼び出し。
6rescanned_macro(((1)*(2)))*(3))

インクルードファイルパス設定処理

template<typename ContextT>
bool locate_include_file(
    ContextT& ctx,
    std::string& file_path, 
    bool is_system,
    char const* current_name,
    std::string& dir_path, 
    std::string& native_name);

インクルードファイルのパスを設定するために呼び出されます。
他の多くのメンバ関数は「行おうとしている処理」や「行われた処理」を捕捉するものですが、この関数はそれ自身が処理を行います。

引数 file_path には #include で指定されたファイル名文字列が渡されます。
ディレクティブ発見時とは異なり、ファイル名を囲む ""<> は文字列に含まれません。

引数 is_system には、 #include <...> ならば true#include "..." ならば false が渡されます。

引数 current_name には、 boost::wave 独自ディレクティブ #include_next が用いられた際に呼び出し元ファイルのパスが渡されます。
#include が用いられた場合は NULL が渡されます。

引数 dir_path に発見したインクルードファイルのディレクトリパスを、引数 native_name に同ファイルのフルパスを設定します。
ただ、 wave::context_policies::default_preprocessing_hooks の既定の動作では、 dir_path にはディレクトリパスが設定されるわけではないようです。
#include <...> ならばファイルのフルパスが、 #include "..." ならば指定したファイル名文字列が設定されるようです。

ファイルパスの設定に成功したならば true を、失敗したならば false を返します。
wave::context_policies::default_preprocessing_hooks の既定の動作では、コンテキストクラスに設定されたインクルードパスを基にファイルの検索を行い、見つかったならばそのフルパスを native_name に設定して true を返し、見つからなければ false を返します。

インクルードファイル解析開始/完了時

template<typename ContextT>
void opened_include_file(
    ContextT const& ctx,
    std::string const& relname, 
    std::string const& absname,
    bool is_system_include);
template<typename ContextT>
void returning_from_include_file(ContextT const& ctx);

opened_include_file はインクルードファイルの解析を開始した時に呼び出されます。
引数 relname には #include で指定されたファイル名文字列が渡され…て欲しいのですが、実際には locate_include_file の引数 dir_path に設定された文字列が渡されるようです。
引数 absname にはインクルードファイルパス設定処理によって設定されたインクルードファイルのフルパスが渡されます。
引数 is_system_include には、 #include <...> ならば true#include "..." ならば false が渡されます。

returning_from_include_file は現在解析中だったインクルードファイルの解析完了時に呼び出されます。

インクルードガード検出時

template<typename ContextT>
void detected_include_guard(
    ContextT const& ctx,
    std::string const& filename,
    std::string const& include_guard);
template<
    typename ContextT,
    typename TokenT
>
void detected_pragma_once(
    ContextT const& ctx,
    TokenT const& pragma_token,
    std::string const& filename);

これらの関数は、コンテキストクラスの言語サポートオプション設定wave::support_option_include_guard_detection が設定された場合のみ呼び出されます。

detected_include_guard は、インクルードファイルの内容全体が次のような記述で囲まれていた場合に呼び出されます。
呼び出されるタイミングは returning_from_include_file の直後です。

#ifndef FOO_H
#define FOO_H

/* ファイル内容 */

#endif // FOO_H
#if !defined(FOO_H)
#define FOO_H

/* ファイル内容 */

#endif // FOO_H

#ifndef#if の前、もしくは #endif の後に空白文字やコメント以外の有効なC++トークンが存在する場合には呼び出されません。
引数 filename にはインクルードガードの設定されているファイルのフルパスが渡されます。
引数 include_guard にはインクルードガードに用いられたマクロ名(上記の例ならば FOO_H)の文字列が渡されます。

detected_pragma_once は、インクルードファイル内で #pragma once の記述が見つかった場合に呼び出されます。
引数 pragma_token には #pragmaトークン情報が渡されます。
引数 filename には #pragma once の記述されているファイルのフルパスが渡されます。

例外送出処理

template<
    typename ContextT,
    typename ExceptionT
>
void throw_exception(
    ContextT const& ctx,
    ExceptionT const& e);

例外を送出すべき際に呼び出され、例外送出処理を行います。
この関数で例外を送出しないようにすると、例外を無視して解析を続行させることができます。

引数 e に送出すべき例外インスタンスが渡されます。
引数の型 ExceptionTwave::preprocess_exception または wave::cpplexer::lexing_exception となり、いずれの型も std::exception から派生しています。

wave::context_policies::default_preprocessing_hooks の既定の動作では e をそのまま throw します。

トークンスキップ処理

template<
    typename ContextT,
    typename TokenT
>
bool may_skip_whitespace(
    ContextT const& ctx,
    TokenT& token,
    bool& skipped_newline);

generated_token 呼び出しの直前に呼び出され、トークンをスキップすべきか否かの判断を行います。
通常は関数名の通り余分な空白文字をスキップするか否かの判断に用いますが、実際は空白文字以外もスキップさせることができます。

引数 token にスキップするか否か判断する対象のトークン情報が渡されます。
このトークン情報は非 const であり、自由に書き換えることができます。

引数 skipped_newline には false で初期化された bool の参照が渡されます。
この関数によって改行をスキップする場合、 skipped_newlinetrue を設定するようにします。

トークンをスキップするならば true を、スキップしないならば false を返します。
wave::context_policies::default_preprocessing_hooks の既定の動作では、何も行わずに常に false を返します。

なお、この関数には generated_token と同様に一番最後にEOIトークンが渡されます。
EOIトークンをスキップしても再びEOIトークンが渡され、 false を返すまで無限ループとなります。
EOIトークンはスキップしないようにしましょう。

シフトJIS対応

ソースファイルの文字コードがシフトJISである場合の問題点とその対処方法。

シフトJISの問題点

boost::wave で文字コードがシフトJIS*4のソースファイルを解析しようとするとうまく解析できないことがあります。
例えば次のようなソースコードです。

// 漢数字表
const char* const kanjiNumbers[] =
    { "一", "二", "三", "四", "五",
      "六", "七", "八", "九", "十" };

「表」や「十」といった文字はシフトJISだと2バイト目が 0x5C 、即ちバックスラッシュ(\)となります。
ご存じの通り、C++ではバックスラッシュはエスケープ文字として利用されています。
Visual C++ 等、シフトJISに対応している開発環境やコンパイラはこれらの文字の2バイト目をエスケープ文字として扱うことなく処理してくれますが、 boost::wave では当然ながらシフトJIS対応などされていません。

C++のソースコード中で2バイト文字を使えるのは主にコメント中と文字列中ですが、これらの場所で上述のような2バイト文字を使うと次のような問題が発生します。

  • コメントの行末に \ があると、C++の仕様で次の行と連結されます。これにより次の行もコメント扱いされてしまいます。
  • 文字列中に \ があるとエスケープ文字として解釈されます。特に文字列末尾にある場合、ダブルクォートをエスケープしてしまい、文字列が閉じられていない扱いになってしまいます。

即ち上述のコードは次のように書かれているようなものとして解釈されてしまいます。(あくまでイメージです)

// 漢数字\x95const char* const kanjiNumbers[] =
    { "一", "二", "三", "四", "五",
      "六", "七", "八", "九", "\x8F\" };

2バイト目が 0x5C となる文字には「ソ」などもあり、コメント中はともかく、文字列中で使わないように制限することは困難です。

対処方法

文字コードがシフトJISであることが問題なので、 boost::wave による解析前にソースコード文字列の文字コードを変換してしまえば問題ありません。
boost::wave はワイド文字列(wchar_t)を扱えないため、変換先文字コードとしてはUTF-8が最適でしょう。

解析するソースコードそのものは、ファイルから読み込んだ後に文字コード変換すればいいでしょう。
しかしそれだけでは、ソースコード内の #include ディレクティブによって boost::wave が内部で読み取るヘッダファイルは文字コード変換できません。
そこで、 boost::wave のヘッダファイル読み込み処理をプログラマ側で用意する必要があります。

boost::wave は、コンテキストクラスクラステンプレート引数 InputPolicyT に渡したクラスを用いてヘッダファイルを読み取ります。
通常では wave::iteration_context_policies::load_file_to_string クラスを用いますが、この引数に文字コード変換対応したクラスを渡すようにします。

なお、この方法でソースコードを解析する場合、トークン文字列を始めとするソースコード内から得た文字列は当然ながらUTF-8になっています。
トークン文字列を出力したり元の文字コードで処理したい場合、下記のいずれかの方法で文字コードを元に戻す必要があります。

コーディング例

後述のサンプル 02_SjisBasic にコーディング例があります。
ここではサンプルのコードについて要点のみ解説します。

まず、シフトJISからUTF-8への文字コード変換処理を用意する必要があります。
方法は色々あるかと思いますが、サンプルでは私が過去に作った簡易的な文字コード変換ライブラリである encoding を用いています。*5

ファイルから文字列を読み取ってUTF-8に変換する処理は関数化しておいた方がいいでしょう。
そしてその関数を利用してコンテキストクラスのクラステンプレート引数 InputPolicyT に渡すクラスを実装します。
加えて、読み取ったトークン文字列を出力したりしたい場合にはUTF-8から標準文字コードへの変換処理も必要になります。
それらをまとめたヘッダファイルがサンプルの common/wave_utf8.hpp になります。

Utf8InputPolicy クラスの記述に戸惑うかもしれませんが、これはこう書くものだと思ってしまっていいです。
wave::iteration_context_policies::load_file_to_string クラスの記述を参考にして、UTF-8への文字コード変換を行う以外は同等の動作になるように記述しています。

そして、作成したファイル読み取り関数を使って解析対象のソースコードを読み込むようにします。

 21
 22
    // ソースファイルを読み込んで文字コードをUTF-8に変換
    const std::string code = wave_utf8::readFileToUtf8(argv[1]);

コンテキストクラスのクラステンプレート引数 InputPolicyT には上述の Utf8InputPolicy クラスを渡します。

 24
 25
 26
 27
 28
 29
 30
 31
    // コンテキスト用意
    typedef
        wave::context<
            std::string::const_iterator,
            wave::cpplexer::lex_iterator< wave::cpplexer::lex_token<> >,
            wave_utf8::Utf8InputPolicy,
            wave::context_policies::default_preprocessing_hooks>
        Context;

解析結果のトークン文字列を出力する際には標準文字コードへ戻すようにします。

 50
 51
 52
 53
 54
 55
 56
        // ソースコード解析しつつ結果のトークンを出力
        Context::iterator_type itrEnd = ctx.end();
        for (Context::iterator_type itr = ctx.begin(); itr != itrEnd; ++itr)
        {
            // トークンはUTF-8なので標準文字コードに戻してから出力
            cout << wave_utf8::convertFromUtf8((*itr).get_value());
        }

これでシフトJISのソースコードを問題なく解析することができます。

ただし、このサンプルでは逆にシフトJIS以外のソースコードをうまく解析できません。
どちらも問題なく解析できるようにするには、元の文字コードを判別して処理を切り分ける必要があるでしょう。

サンプル

Visual C++ 2012/2013 用のサンプルコードをGitHubにて公開しています。

以下に述べる5つのサンプルプログラムのソースコードが入っています。
いずれのサンプルも、起動パラメータに渡されたソースファイルの内容を解析します。

01_Basic

基本的な使い方に載せたコードそのものです。

02_SjisBasic

シフトJIS対応対処方法に書いた方法を 01_Basic のコードに対して適用したサンプルです。

03_IgnoreDirectiveHook

フッククラスクラス概要に載せた、あらゆるディレクティブを処理しないようにするフッククラス IgnoreDirectiveHook を用いたサンプルです。

04_OutputHookInfos

フッククラスの各メンバ関数が呼び出された際に各引数の情報を出力するサンプルです。
フッククラスの各メンバ関数がどのようなタイミングで呼び出されているか調べる上で役立ちます。

05_MakeIncludeTree

他よりも少し実用的なサンプルです。

渡されたソースファイル中の #include を辿り、 #include のツリーを boost::property_tree::ptree 型のインスタンスに構築しています。
サンプルでは構築したツリーを boost::property_tree::info_parser::write_info を用いて出力しています。

例えばこのサンプルプログラムに #include <cstdio> とだけ書かれたファイルを渡すと、次のように出力されます(環境により出力内容は異なります)。

<cstdio> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\cstdio"
{
    <yvals.h> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\yvals.h"
    {
        <xkeycheck.h> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\xkeycheck.h"
        <crtdefs.h> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\crtdefs.h"
        {
            <sal.h> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\sal.h"
            {
                <ConcurrencySal.h> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\ConcurrencySal.h"
            }
            <vadefs.h> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\vadefs.h"
        }
        <use_ansi.h> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\use_ansi.h"
    }
    <stdio.h> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\stdio.h"
    {
        <crtdefs.h> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\crtdefs.h"
        <swprintf.inl> "C:/Program Files\\Microsoft Visual Studio 12.0\\VC\\include\\swprintf.inl"
    }
}

boost::property_tree については次のサイトが参考になります。

その他

その他、実装する上で留意すべき点など。

Visual C++ で警告 C4996 が大量に出る

警告 C4996 は、セキュリティ上問題のある(古い)関数が使われている場合に出る警告です。
詳しくはMSDNライブラリを参照してください。

とはいえ boost::wave のソースコードを書き換えるわけにもいかないので、現状ではプロジェクトのプロパティなどでこの警告を無視するように設定するしかありません。

コンパイルが通るはずのソースコードで例外になる

次のような例外が出力されていませんか?

  • error: ill formed preprocessor directive: ...
  • warning: last line of file ends without a newline

これらはいずれも、最終行が改行で終わっていないファイルが存在するために発生する例外です。
boost::wave では最終行が改行で終わっていないファイルを不正なフォーマットとして扱います。

最終行が改行で終わっていないファイルも問題なく解析したい場合、読み込んだソースコード文字列の末尾に強制的に改行を付与してしまうという手があります。
その場合、シフトJIS対応対処方法でも書いたように、コンテキストクラスのクラステンプレート引数 InputPolicyT に渡すクラスも変更する必要があります。

*1 特にメンバを持たないクラスであるならば既定のコピーコンストラクタで問題ありません。
*2 例えば空文字列のイテレータなど。
*3 デフォルトコンストラクタを用いるならば引数指定は省略可能です。
*4 CodePage932等、シフトJISがベースの文字コードも含む。
*5 encoding 内で用いている <codecvt> ヘッダがC++17で非推奨扱いとなったため、 encoding 自体のリポジトリは削除しました。