Cプリプロセッサの挙動の違い

Cプリプロセッサというのはgcc, clang, MSVCなどの処理系ごとに微妙に異なります。
#pragmaディレクティブなどは、各コンパイラ等の独自拡張機能をサポートする為などに使われていますが、Cプリプロセッサの差というのはサポートする機能の有無やキーワードの違いだけには留まらないようです。

今日は、偶然MSVCが#pragma内のトークンを展開するという個人的に意外な挙動を発見したのでこれを書いておきます。

次のような例です。

#define print(msg) message(msg)
  
int main() {
#pragma print("hello world!")
};

このように記述した場合、gccとclangでは

warning: unknown pragma ignored [-Wunknown-pragmas]
#pragma print("hello world!")

として無視されますが、MSVCだけは

hello world!

と出力されます。
これは
#pragma print("hello world!")

#pragma message("hello world!")
に展開された為です。
このように、MSVCでは#pragma内のトークンが展開されます。

少々検索をしたところ実際にそれを利用して、行番号とメッセージを出力させてデバッグ補助等を行うような手口が紹介されたりしていました。(恐らくMSVCしか使っていないユーザによる、バッドノウハウ)

#pragma message(msg)
によるメッセージの出力はgccやclangもサポートしていますが、この記述を続ける限り、どう足掻いてもgccやclangではこのマクロは何の意味も成しません。
これは、完全にMSVCのプリプロセッサの“挙動“に依存したコードです。

gccやclangでは
#pragma トークン名
を展開しない代わりに、

#define pragma(x) _Pragma(#x)
#define print(x) pragma(message x)
print("hello");

のように_Pragmaにパラメータを文字列化して渡すようにしてあげると
#pragma message("hello")
として展開してくれます。

しかし、この記述を行った場合、今度はMSVCで期待通りに動作しません。
MSVCでこの機能を提供するキーワードは_Pragmaではなく、__pragmaだからです。
尚且つ、MSVCの__pragmaは文字列化を行わずに渡す必要があるようです。

そこで、gcc又はclangの場合とMSVCで場合分けを行います。

#ifdef _MSC_VER // MSCVの場合
#define pragma(x) __pragma(x)
#define print(x) pragma(message(x))
#else // MSVC以外の場合
#define pragma(x) _Pragma(#x)
#define print(x) pragma(message x)
#endif

int main() {
    print("hello");
};

このようにすることで初めて全ての処理系で期待通りに動作するようになります。

実際のところ、プリプロセッサの微妙な処理系ごとの違いというのは、様々な環境で使われるライブラリ等を書く上では面倒だと思います。
この他にも挙動において落とし穴があることは十分に考えられます。
特にMSVCのプリプロセッサは、キーワードや挙動がgccやclangと異なる部分が多い気がするので、プリプロセス処理の書き分けがそれなりに必要になるなかなかのクセモノですね。

自分はCプリプロセッサにはあまり詳しくないのですが、実際にはCプリプロセッサにも#pragma内のトークンの展開を行うかは厳密に規定されているかもしれません。
その場合は、バグレポート等を出すことも出来ると思います。
判ったらまた追記します。
といっても現実の挙動がこうである以上、当分はどうにもなりませんが…。