プリプロセッサで型名を自動整形 (C/C++ 両対応)

C++ と C で同じ内容の型を定義したいときがある。しかも、C++ ではちゃんと名前空間に入れて、さらに C では名前空間に対応するユニークな名前で修飾しておきたい場合とかも。というわけでこれに対応するためのマクロを Boost.Preprocessor を使って書いてみよう。
その前に、このマクロを使って書いたサンプルのヘッダファイルの一部がこんな感じ。(nvm は単なるプログラムの内部名称。)

/** (前略) **/
#define nvm_ccompat_namespace (nvm, (kernel, NVM_CCOMPAT_NAMESPACE_TAIL))
#include <nvm-fix/ccompat/namespace_begin.hpp> /* { */

typedef struct NVM_CCOMPAT_TYPE_DECL_TAG(sample_t)
{
    /* .... */
} NVM_CCOMPAT_TYPE_DECL(sample_t);

typedef struct NVM_CCOMPAT_TYPE_DECL_TAG(sample2_t)
{
    /* .... */
    NVM_CCOMPAT_TYPE(sample_t) mem;
} NVM_CCOMPAT_TYPE_DECL(sample2_t);

#include <nvm-fix/ccompat/namespace_end.hpp>   /* } */
/** (後略) **/

実装はかなり好みに左右される。前もってヘッダをインクルードした上でマクロ宣言を使っても良いのだが、ここで定義した nvm_ccompat_namespace の後片付けをフレームワーク側ですることを考慮し、このように namespace_begin.hpp と namespace_end.hpp の 2 つで囲う実装になっている。
マクロは自動的に後片付けされる上にそもそもソースコード側でマクロを使わないために作ったヘッダなので、これらの生成された型を使用するには C と C++ 各々の自然な名前を使用する。

/* 使用法 : C++ の場合
   using namespace による省略も使えるが、
   名前空間に配置されていることを確認するために明示的な記述をした。 */
#include <nvm/sample.hpp>

void sample(nvm::kernel::sample2_t value)
{
    nvm::kernel::sample_t m = value.mem;
}
/* 使用法 : C の場合 */
#include <nvm/sample.hpp>

void sample(nvm_kernel_sample2_t value)
{
    nvm_kernel_sample_t m = value.mem;
}

見てわかるとおり、C++ においては名前空間内に修飾されない型が配置されるのに対して、C では名前空間に対応する名前 (この場合 nvm_kernel_) で修飾を受けた型名を使うことになる。
マクロ展開された結果はおおむね次の通り。(空白、改行などは一部分かりやすいように改変している。)

/** (前略) **/

namespace nvm {
namespace kernel {

typedef struct TAG_sample_t
{
    /* .... */
} sample_t;

typedef struct TAG_sample2_t
{
    /* .... */
    ::nvm::kernel::sample_t mem;
} sample2_t;

}}

/** (後略) **/
/** (前略) **/

typedef struct TAG_nvm_kernel_sample_t
{
    /* .... */
} nvm_kernel_sample_t;

typedef struct TAG_nvm_kernel_sample2_t
{
    /* .... */
    nvm_kernel_sample_t mem;
} nvm_kernel_sample2_t;

/** (後略) **/

では、これをどのように実装しているかを見てみよう。

実装

勘の良い人なら (#define nvm_ccompat_namespace の宣言で) わかるように、これは Boost.Preprocessor の list 機能を使っている。勘違いされがちなのだが、Boost は必ずしも C++ 向けのライブラリではない。特に Boost.Preprocessor は、標準準拠の C コンパイラならすべての機能を使うことができる。
nvm_ccompat_namespace の宣言で使っているデータ構造は Boost.Preprocessor では list と呼ばれるデータ構造だ。*1このデータ構造を、プリプロセッサを使って弄り倒してみる。
では namespace_begin.hpp の方を見ていこう。

list の使用宣言
#define NVM_CCOMPAT_NAMESPACE_TAIL BOOST_PP_NIL
#include <boost/preprocessor/list.hpp>

NVM_CCOMPAT_NAMESPACE_TAIL は単に名前付けのルールを統一するためのものだ。実際のところ、マクロは展開される (使用する) 場所で実際に展開されるため、namespace_begin.hpp や の前に NVM_CCOMPAT_NAMESPACE_TAIL や BOOST_PP_NIL 識別子を (マクロ宣言内で) 使っていても何の問題もない。

名前空間スコープ

次に、スコープを定義しよう。

#ifdef __cplusplus /* C++ のみ */
#define NVM_CCOMPAT_NAMESPACE_BEGIN_I(r,data,elem) namespace elem {
BOOST_PP_LIST_FOR_EACH(NVM_CCOMPAT_NAMESPACE_BEGIN_I,:,nvm_ccompat_namespace)
#undef NVM_CCOMPAT_NAMESPACE_BEGIN_I
#endif

ここでは C++ に限り名前空間を展開する。ここで、あらかじめ定義されていたマクロである nvm_ccompat_namespace がリストとして評価、さらに各要素がマクロ展開される。言うまでもないけど、C 言語の場合は __cplusplus の定義に阻まれてこの場所は空白に展開される。

   BOOST_PP_LIST_FOR_EACH(NVM_CCOMPAT_NAMESPACE_BEGIN_I,:,nvm_ccompat_namespace)
>> BOOST_PP_LIST_FOR_EACH(NVM_CCOMPAT_NAMESPACE_BEGIN_I,:,(nvm, (kernel, BOOST_PP_NIL)))
>> NVM_CCOMPAT_NAMESPACE_BEGIN_I(nvm) NVM_CCOMPAT_NAMESPACE_BEGIN_I(kernel)
>> namespace nvm { namespace kernel {
型の定義名と使用名

型に関しては単純な定義ルールを採用しても良いのだが、名前空間に関連する混乱を避けるため、次のルールに従う。

  • 宣言時 (NVM_CCOMPAT_TYPE_DECL マクロで定義)
    • C++ では型の宣言時に名前を保持する
      • nvm::kernel 名前空間における sample_t → sample_t
    • C では型の宣言時に名前空間をアンダースコアで区切ったものをプレフィックスとして配置する。
      • nvm::kernel 名前空間における sample_t → nvm_kernel_sample_t
  • 使用時 (NVM_CCOMPAT_TYPE マクロで定義)
    • C++ では型の使用時に、明示的にグローバルスコープから名前空間を辿った名前で展開する。
      • nvm::kernel 名前空間における sample_t → ::nvm::kernel::sample_t
    • C では宣言時と等価。
      • nvm::kernel 名前空間における sample_t → nvm_kernel_sample_t

この両方を満たすため、2 つのマクロを使用することに。まずは宣言時、NVM_CCOMPAT_TYPE_DECL マクロから。

#ifdef __cplusplus /* C++ の場合 */
#define NVM_CCOMPAT_TYPE_DECL_I
#define NVM_CCOMPAT_TYPE_DECL(clsname) clsname
#else              /* C 言語の場合 */
#define NVM_CCOMPAT_TYPE_DECL_I(d,data,elem) elem##_
#define NVM_CCOMPAT_TYPE_DECL(clsname) BOOST_PP_CAT(BOOST_PP_LIST_CAT(BOOST_PP_LIST_TRANSFORM(NVM_CCOMPAT_TYPE_DECL_I,:,nvm_ccompat_namespace)),clsname)
#endif

ここでは C 言語のほうが (名前空間プレフィックスとして付加するために) 複雑な実装になっている。
C++ のほうは単純。clsname を clsname と展開するだけ。ここでは C/C++ 両言語で全く同じマクロを定義し、後で #undef するため、ダミーのマクロ定義も含んでおこう。
C 言語の方は、プリプロセッサレベルで次のように動作する。

  • 名前空間リストの各要素を、最後に _(アンダースコア) が付くように設定する。(BOOST_PP_LIST_TRANSFORM)
  • リストの全要素を 1 つの識別子として連結する。(BOOST_PP_LIST_CAT)
  • 最後に、clsname を連結する。(BOOST_PP_CAT)

具体的な展開の内容は次のようになる。

   BOOST_PP_CAT(BOOST_PP_LIST_CAT(BOOST_PP_LIST_TRANSFORM(NVM_CCOMPAT_TYPE_DECL_I,:,nvm_ccompat_namespace)),sample_t)
>> BOOST_PP_CAT(BOOST_PP_LIST_CAT(BOOST_PP_LIST_TRANSFORM(NVM_CCOMPAT_TYPE_DECL_I,:,(nvm, (kernel, BOOST_PP_NIL)))),sample_t)
>> BOOST_PP_CAT(BOOST_PP_LIST_CAT((NVM_CCOMPAT_TYPE_DECL_I(nvm), (NVM_CCOMPAT_TYPE_DECL_I(kernel), BOOST_PP_NIL))),sample_t)
>> BOOST_PP_CAT(BOOST_PP_LIST_CAT((nvm_, (kernel_, BOOST_PP_NIL))),sample_t)
>> BOOST_PP_CAT(nvm_##kernel_,sample_t)
>> BOOST_PP_CAT(nvm_kernel_,sample_t)
>> nvm_kernel_sample_t

リファレンスを片手に読むと、順番にリストや識別子が変形していることがわかるだろう。
さて次は使用時の型名だ。こちらはさほど難しくはない。

#ifdef __cplusplus /* C++ の場合 */
#define NVM_CCOMPAT_TYPE_I(d,data,elem) ::elem
#define NVM_CCOMPAT_TYPE(clsname) BOOST_PP_LIST_FOR_EACH(NVM_CCOMPAT_TYPE_I,:,nvm_ccompat_namespace)::clsname
#else              /* C 言語の場合 */
#define NVM_CCOMPAT_TYPE_I
#define NVM_CCOMPAT_TYPE(clsname) NVM_CCOMPAT_TYPE_DECL(clsname)
#endif

C 言語版は宣言時の型名をそのまま使えるのでそこへのリダイレクト。C++ においては、BOOST_PP_LIST_FOR_EACH を使って、次のように展開している。

   BOOST_PP_LIST_FOR_EACH(NVM_CCOMPAT_TYPE_I,:,nvm_ccompat_namespace)::sample_t
>> BOOST_PP_LIST_FOR_EACH(NVM_CCOMPAT_TYPE_I,:,(nvm, (kernel, BOOST_PP_NIL)))::sample_t
>> NVM_CCOMPAT_TYPE_I(nvm) NVM_CCOMPAT_TYPE_I(kernel) ::sample_t
>> ::nvm ::kernel ::sample_t
>> ::nvm::kernel::sample_t (←マクロ展開ではなく、C++ 言語における型名の解釈)

BOOST_PP_CAT 系を使っていないのは、これらは識別子を連結するためにしか使えず、また C++ の "::" は演算子であるため、自動的に、かつ適切に区切られるから。

構造体タグ

上では、今まで定義しなかったマクロが 1 つだけ使われている。NVM_CCOMPAT_TYPE_DECL_TAG(clsname) だ。これは無名構造体になることを避けるための構造体タグを名付けるためのユーティリティマクロであり、次のように定義される。

/* 両言語共通 */
#define NVM_CCOMPAT_TYPE_DECL_TAG(clsname) BOOST_PP_CAT(TAG_,NVM_CCOMPAT_TYPE_DECL(clsname))

これだけで OK。

namespace_end.hpp

namespace_end.hpp の実装は極めて単純だ。ここではソースコードの一部をそのまま貼り付ける。

#ifdef __cplusplus
#define NVM_CCOMPAT_NAMESPACE_END_I(r,data,elem) }
BOOST_PP_LIST_FOR_EACH(NVM_CCOMPAT_NAMESPACE_END_I,:,nvm_ccompat_namespace)
#undef NVM_CCOMPAT_NAMESPACE_END_I
#endif
#undef NVM_CCOMPAT_TYPE
#undef NVM_CCOMPAT_TYPE_I
#undef NVM_CCOMPAT_TYPE_DECL
#undef NVM_CCOMPAT_TYPE_DECL_I
#undef NVM_CCOMPAT_TYPE_DECL_TAG
#undef NVM_CCOMPAT_NAMESPACE_TAIL
#undef nvm_ccompat_namespace

名前空間スコープを終了させるのは開始するものと使用するマクロの内容が異なるだけ。それ以外は、namespace_begin.hpp で定義されたマクロ等を #undef するだけと単純そのもの。実際には namespace_begin.hpp、namespace_end.hpp ともに幾つかの安全策を施しているが、ここでは省略する。

まとめ

ここまでやってもこれを使う機会があまりない (内部構造体を外にだすこと自体が少ない) のは残念だが、見た目としては非常に面白いものに仕上がった。Boost.Preprocessor の力も再確認できたことだし、今回はこれで満足。

*1:LISP における list の表現とかなり似ているのがお気に入り。