こんにちは、高木です。
いつものように、PHPでC言語の前処理を行う話題が続きます。今回は、C言語規格のバージョン間や処理系間の移植性を高める方法について考えてみることにします。具体的な題材があった方がいいので、C11から導入された_Static_assertをC99やそれ以前のバージョンでも使えないか考えてみましょう。
C11での予約語はあくまでも_Static_assertですが、<assert.h>でstatic_assertマクロが定義されていること、C++ではstatic_assertが予約語だということもあってstatic_assertの方が馴染みがあるかもしれませんね。
それでは、早速作っていきましょう。
0 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 |
<?php function static_assert(string $expr, string $message = NULL): void { global $c_version; if ($c_version < 11 || 89 <= $c_version) { static $count = 1; echo "char static_assert_{$count}[-!($expr)];"; if (!is_null($message)) echo " /* $message */"; ++$count; } else if ($c_version < 18) { if (is_null($message)) { $message = $expr; } echo "_Static_assert($expr, \"$message\");"; } else { if (is_null($message)) echo "_Static_assert($expr);"; else echo "_Static_assert($expr, \"$message\");"; } echo PHP_EOL; } ?> |
今回作ったstatic_assert関数では、グローバル変数$c_versionでC言語規格のバージョンを指定するようになっています。4桁で西暦年を書いてもいいのですが、できれば通称どおりの表記にしたかったので今回は2桁で指定するようにしています。
C言語規格のバージョンとして想定しているのは、89, 90, 95, 99, 11, 17, 18, 23です。20世紀のバージョンの方が大きな値になってしまうので、単純な大小比較では新旧を判別することができません。そこで、$c_version < 11 || 89 <= $c_versionのようにしています。
C99以前には_Static_assertがありませんので、別の方法で代替しなければなりません。本来なら関数の宣言を使う方がメモリを消費せずに済むのでそうしたかったのですが、構造体の中でもstatic_assert関数の呼び出しを記述したいので、今回は配列を使うことにしました。
ちょっと変わった書き方ですが、条件式$exprが真の場合は配列の要素数が0に、偽の場合は-1になるようにしています。配列の要素数が負の場合は意図的にコンパイルエラーを発生させるようにしています。
規格合致プログラムにするには若干制約があって、構造体の最後にstatic_assertの呼び出しを書かなければなりません。また、ひとつしか書くことができません。しかし、たとえばGCCを使う場合には、構造体の中のどこに書いても、また、いくつ書いても(警告が出ることはありますが)エラーにはなりません。メモリも消費しませんので期待通りの振る舞いになります。
C11とC17(C18)では_Static_assertが導入されました。しかし、2番目の引数は必ず指定しなければなりません。$messageが省略された場合、$exprの内容を文字列化して$messageの代わりにしています。本来なら$exprに「\」や「”」が含まれている場合はエスケープしないといけないのですが、今回の話題の本質ではないの省略しました。
C23からは_Static_assertの第2引数を省略できますので、素直にそのまま展開しています。
今回は_Static_assertを題材にしてみましたが、同じようにすれば、バージョンや方言の違いを吸収して移植性を高めることができます。