2013年2月23日土曜日

処理系に応じた…、そんな時はCプリプロセッサーを使え、但しほどほどにな( ・`д・´)

最近(略)でしへ。

C++がCからほぼそのまま受け継いでいる機能の1つに「Cプリプロセッサー」がある。関係者の間ではCPPと略される事もある。もちろんここでのCPPはC Plus PlusではなくC Pre-Processorだ。

一般にC++の使い手たちが「黒魔術」と言えば、このCPPを応用した錆びれつつある技術の応用が含まれる事がある。しかし、CPPは所詮はごく簡単ないわば簡易言語であり、乱用しなければC++プログラマーの良き道具の1つに過ぎない。CPPは黒魔術では無くせいぜい古代魔法である、節度を守りRAIIの精神に則り清く正しく美しく使われる分には、だが。

さて、早速実用の例を示そう。

例えばOSXとGNU/LinuxとWindowsを何れも使うだけならばほぼ自在かもしれない君がC++のプログラムを書こうとしている。しかし不幸な事にOSが提供するAPIを直接使用したい状況に陥ってしまったとする。どうしたら良いか?

最も簡単な答えがCPPだ。StackOverflowからその答えを紹介する。
この質問と答えではiOSの分岐も盛り込んでくれているが、もののついでにAndroidも分岐したくなっただろうか。その質問と答えもStackOverflowから紹介する。
この例の様に、C++のソースコードそのものを動的に変化を付けて生成したい場合にCPPは便利に使われる事がある。

先の事実上のOSによる処理の分岐は、実はC++のコンパイラーがintがどうしたとかmainはどこだとか関数の呼び出しがどうしたこうしたとか、そういったいわば本当の翻訳作業に取り掛かるよりもに処理される。

どういう事か?

CPPの(特にPPの部分の)意味を考えれば自明となる。C(Cの) Pre(≈事前の)-Processor(≈処理者)

そんな訳で、C++(というかそれ以前にという意味でC)のにソースコードを処理してくれる簡易言語がCPPだ。ちなみに、一般にはこうしたプログラミング技法を指してメタプログラミングと呼ぶ。

先のコードもC++のソースコードの対象機械語への翻訳の前に、#if#ifdef)〜#endifを基本としたif文風の条件分岐によって、処理系に応じた翻訳時のソースコードの修正を自動化しているだけの事。

#ifの条件式に登場するシンボル(__APPLE__だとか__linuxだとか)はもうお察しかもしれないがCPPにおける変数の様なものだ。

どうでも良い余談として、#elifだなんて最近誰かの講義で習ったばかりの様な気がしたかもしれない。そうだとすればきっとシェルスクリプトだろう。CPPはシェルスクリプトよりも使える言語機能が少ないだけとても簡単だ。

#if … 条件分岐。与えられる条件式に応じて#elifまたは#elseまたは#endifまでを有効なソースコードブロックとして採用する。

#elif … いわゆる else if に相当する。#else〜#endifの内部に#if〜#endifをネストしても同じ事。可読性を考慮して使うべし。

#else … いわゆる else に相当する。

#ifdef … #if defined()の省略形。defined()はシンボルが#defineされていれば1、居なければ0に展開されるCPP組み込み関数風のそれ。

#define … #undefまたはソースコードの終端まで有効な変数風のシンボルや関数風のシンボルを定義する。定義方法は後述。

#undef … #defineされたシンボルを無かった事にする。CPPを黒魔術にしないC++erのRAII精神に基づいて言えばnewに対するdeleteの様なものである。但しスマートポインターやデストラクターはCPPの世界に存在しない。

## … #define中でのみ使える二項演算子で、前後をホワイトスペースを除去して連結したソースコードを生成する。

# … #define中でのみ使える右結合性の単項演算子で、ダブルクォーテート記号(")で囲った文字列リテラルのソースコードを生成する。

#defineの使い方は#define A#define B bbb#define C(cx,cy,cz) cccの3つ。順に簡単に解説しよう。

A中身の無いシンボルのみを定義する。主に#ifdefでその後に分岐する為のフラグを仕込むのに使われる。

BはCPPで変数風のシンボルを定義する。実際のところは#undefしなければ書き換えられないので変数じゃなくて定数だ。

CはCPPで関数風のシンボルを定義する。

A、B、Cの何れもソースコード中で何の記号も付けずにシンボルを書くだけで突然呼び出して使えてしまう。実際にやってみよう。

もののついで、D、E、Fも付けておいた。やってみればそれがどういう意図で紹介されたのか分かるだろう。

#define A
#define B bbb
#define C(cx, cy, xz) ccc1(cx); ccc2(cy, cz)
#define D(dx, dy, dz) \
d1( # dx );\
d2( ddd_ ## dy ##_2 );\
d3( dz )
#define E 123
#define F 0x123

int main()
{
  #ifdef A
    defined_a();
  #else
    undefined_a();
   #endif
  A;
  B;
  #undef B
  B;
  C(1,2,3);
  D(alpha, beta, gamma);
  #if E < F
    easy_integer_comparsion_is_available();
  #endif
}

こんなソースを書いたら、GCCClangなら`gcc -E hoge.cxx`とオプションを付けて、MSVC++ならスタートメニューからコマンドライン版のMSVC++開始用のバッチファイルを起動したcmd.exeから`cl /E hoge.cxx`とすれば、(いったいこのソースコードはどうプリプロセスされてから機械語への翻訳作業が始められるのか、気になるだろう?)が標準出力に得られる。わざわざ示すのも面倒なので自分でやってみる事。

注意すべき事として、#defineは「単なるソースコード文字列の置換」でしかない。無闇な#defineの乱用はユーザーに混乱を与える。有名どころではMicrosoftがWindows APIでやらかしたmin/maxマクロ問題が挙げられる。この問題はもしも君がWindowsでC++コードを書くのならば知って置かなければやがて君を苦しめる事になるかもしれない。

Windows APIにおけるmin/max問題の様な悪例もあるが、先に示した対象の処理系(ここでは≈OS)によって翻訳されるソースコードレベルの分岐を用意に実現できるOSやバージョンを示すシンボルを、翻訳時に環境に組み込まれたシンボルで提供し、プログラマーが対象環境ごとに別々のソースコードファイルを用意する手間を省くのに役立ったり、或いはちょっとした単調なソースコードの繰り返し記述を#define〜#undefを活用して楽に記述するのにもCPPは役立っている。しかも翻訳時にそれが自動化できるのだから素敵な機能とも言えよう。

但し、くれぐれもCPPの活用はほどほど、控え目な程度に留める事。そうしないとソースコードの保守性や可読性を著しく悪化させ\(^o^)/とか/(^o^)\を後々招く事になるだろう。

さて、これで実用には十分なヒントになったと思うが、もちろんここで紹介しただけがCPPのすべてではない。必要に応じて言語仕様や先人のCPPに関する記事を読むと良い。また、君がもう少しC++に慣れて来た頃には、C++におけるメタプログラミングとCPPの活用やライブラリーの実装も少しずつ読み解いて見ても面白さを覚えるかもしれない。

…しつこい様だがくれぐれも乱用しない事。乱用すればやがて誰かが死ぬ、それは自分かもしれないし、会社の後輩かもしれないが、何れ誰かを殺す事になるだろう。

0 件のコメント:

コメントを投稿