2012年12月8日土曜日

【C++ Advent Calendar 2012】 8日目 「C++ Compiler Farm の紹介」&「キャストの復習」

このエントリは C++ Advent Calendar 2012 8 日目の記事です。

C++ Advent Calendar 2012 というからには普通 C++ のネタを提供するものですが、一応、C++ 関連ではあるのですが言語仕様でもなければライブラリでもないネタです。それだけではなんなので一応小ネタとして「キャストの復習」という内容も書いてみます。

C++ Compiler Farm の紹介

既に一度 Twitter 上で流したネタではあるのですが、「C++ Compiler Farm」 というサイトを作ってみました。

C++ は言語仕様が複雑であり、かつ、標準実装や唯一の実装のようなものが存在しないこともあり処理系によって挙動がまちまちである、というのは C++er は身に染みて良く知っていることだと思います。それでは皆さんの周辺ではコンパイラは何種類くらい利用可能でしょうか?無償利用可能なコンパイラに限っても世の中に結構な数があるわけですがそんなにたくさん常用できる状態にはない、という人も結構いるのではないでしょうか? C++ Compiler Farm はオンラインで複数の C++ コンパイラによるコンパイル結果、実行ファイルの実行結果を確認できるサービスです。

以前 Twitter 上で流した時点ではコンパイル、実行結果の確認はできる、という状態でしたが、結果を後から参照することができませんでした。今回機能強化を果たし、http://ccf.myhome.cx:5000/result/1 のようなリンクで実行結果を後から参照することができるようになりました。これでコンパイラによって挙動が違う、などと言った時に他の人に結果を見せやすくなるんじゃないかな、と期待しています。

コンパイラ無選択状態でも実行可能だったり、全ての結果が保存されたり、編集できなかったりまだまだ低機能ですが利用者がちょっとでも居そうであればちょこちょこやっていこうかと思っています。なお関連ソースコードは https://github.com/yak1ex/ccf で公開しています。

C++ Advent Calendar で紹介しておきながらあるまじきことですが、ほとんど Perl で実装されています。なので概要だけ構成を説明しておくと以下の図のような構成になっています。

AWS EC2 上で Windows / Linux サーバ Micro instance 各1台。それぞれでコンパイルサーバが実行されており、Web サーバは Linux 側。ブラウザ上の Javascript と協調しながら処理する感じです。サーバ側では非同期処理フレームワーク AnyEvent を使っていますので、C++er としては Boost.Asio とかで書けると格好いいところなんですが。サンドボックス部分だけ、Google Chrome のオープンソース実装である Chromium の C/C++ コードを一部修正して使用しています。これで system("rm -rf /"); とか入力されても問題ないようになっています(http://ccf.myhome.cx:5000/result/7)。……そのはず、です。Windows 側はメッセージなしで黙って無視される形ですね。実行時間、使用メモリについても制限をかけています。相変わらず Amazon Web Services 無料範囲内での運用で、使用資源を絞った状態ですが以前よりはちょっと緩めました。この辺は調整だと思っていますので使用実績が増えれば考えるための材料も増えるかと思っています。

「C++ Compiler Farm」を紹介させて頂きました。皆様の C++ life に少しでも役立てば幸いです。

キャストの復習

ということで小ネタ「キャストの復習」です。最初に言っておきますが、規格上どうか、という話であって、実装上は概ね変わらないとかそういう割と役に立たない話になります。また、アラインメントについて省略したり正確な表現でなかったりします。

さて、C++ キャストは以下の 4 種類あります。

  • const_cast
  • dynamic_cast
  • reinterpret_cast
  • static_cast

このうち const_cast は const 外し、dynamic_cast は安全なキャストであるかどうかを判定できる、という点で位置づけは割と明快です(dynamic_cast の使いどころはどこか、というのはそれはそれで議論になりそうですが)。ということでたまに使い分けで議論になる reinterpret_cast と static_cast について、特にポインタの場合について掘り下げてみようと思います。

が、その前に C-style キャストについても確認しておきましょう。C++ においては C-style キャスト (type)value は以下の C++ キャストとして解釈可能なもののうち先にあるものと解釈されます(14882:2011 5.4p4)。

  • const_cast 1回
  • static_cast 1回
  • static_cast 1回 + const_cast 1回
  • reinterpret_cast 1回
  • reinterpret_cast 1回 + const_cast 1回

つまり文脈によってどのように解釈されるかが変わります。

C 言語において割と暗黙のうちに仮定されているんじゃないかという前提として、ポインタのキャストでは指す位置は変わらない(値は変わらず解釈が変わる)、というものがある気がします(注:C 言語においてもchar* への変換以外規格上その保証はありません(9899:1999, 9899:2011 6.3.2.3p7)。ついでに strict aliasing rule 的に char* 系以外の別の型へのポインタ経由でアクセスすると未定義動作です(9899:1999, 9899:2011 6.5p7)。一方で、C++ においてはポインタのキャストでその値(指している場所)が変わる場合が有り得ます。

以下のような継承がされているクラス群がある場合、

struct A { int n; };
struct B { int n; };
struct C : A, B { int n; };

C 型のオブジェクト内に A 型、B 型のオブジェクトが含まれる形となり、かつ先頭位置を C 型と共有可能なのは A 型か B 型かいずれか一つしかないことになります。規格上定義されていませんが典型的にはメモリレイアウトは以下の図のようになります。

図のようにA型、B型の順に並んでいるとして、C* を B* に static_cast すると C の中にある B の先頭を指すことになり、つまり、指す位置が変わります。逆に B* を C* に static_cast しても指す位置が変わります。この場合、もともと C 型のオブジェクト中の B 型オブジェクトを指していない場合などは不正な位置を指すことになります。つまりこのキャストはいつでも安全とは言えないのですが、static_cast はstandard conversion として規定されている型変換の逆方向のキャストもできると規定されている(14882:2003 5.2.9p6, 14882:2011 5.2.9p7)ため一律コンパイル可能です(もちろん不正な位置を指す場合には未定義動作ですが)。つまり「static_cast ならば安全なキャストだ」というわけではありません。

では static_cast の立ち位置とはなんでしょう?上の段落で単なる「キャストする」ではなく「static_cast する」と明示したことに気付かれたでしょうか?C++03 以前では、reinterpret_cast でのポインタのキャストについてはヌルポインタがヌルポインタのままであること(14882:2003 5.2.10p8)、T1* → T2* → T1* で元に戻ること(14882:2003 5.2.10p7)以外は未規定(unspecified)であり可搬性のあるコードを書くならば reinterpret_cast は使うな、が基本でした。つまり、上のような B* → C* あるいは C* → B* についても(続けてやれば元に戻ること以外) reinterpret_cast の結果について言えることはありませんでした。つまり(未定義動作の場合もあるけど)結果が規定されている static_cast と規定されていない reinterpret_cast という位置づけだった訳です。

C++03 以前において例えば char* を unsigned char* にキャストする(規格上)可搬性のある方法は、void* を経由して static_cast する方法でした。

char *pc;
// unsigned char* upc = static_cast<unsigned char*>(pc); // COMPILE ERROR
unsigned char* upc = static_cast<unsigned char*>(static_cast<void*>(pc));

void* への変換は指す位置が変わらないという規定があります(14882:2003 4.10p2)。一方、void* へ変換して元の型に戻すと同じ場所を指すという規定もあるため(14882:2003 5.2.9p10)、void* からのキャストも指す位置は変わらないことになります。一方、以下のコードもコンパイルは通りますが、

char *pc;
unsigned char* upc_bad1 = (unsigned char*)pc;
unsigned char* upc_bad2 = reinterpret_cast<unsigned char*>(pc);

この C-style キャストは(static_cast ではキャストできない変換なので)前述の通り reinterpret_cast として解釈されます。これも前述の通り reinterpret_cast では結果が保証されないためこのコードは可搬性がありません(pc と upc_bad* で同じ位置を指している保証がない)。

というのが、C++03 以前の話。C++11 では reinterpret_cast の規定が変わりました(14882:2011 5.2.10p7)。

An object pointer can be explicitly converted to an object pointer of a different type. When a prvalue v of type “pointer to T1” is converted to the type “pointer to cv T2”, the result is static_cast<cv T2*>(static_cast<cv void*>(v)) if both T1 and T2 are standard-layout types (3.9) and the alignment requirements of T2 are no stricter than those of T1, or if either type is void. Converting a prvalue of type “pointer to T1” to the type “pointer to T2” (where T1 and T2 are object types and where the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value. The result of any other such pointer conversion is unspecified.

太字下線部分が大体追加された規定で、standard-layout type へのポインタ間の場合、void* を経由する static_cast 2回と等価、つまり↑で可搬性がある方法としていたものになります。standard layout type は規格の範囲で(パディング等はあるけど)メモリレイアウトが決まる型のことです。char, unsigned char については standard-layout type なので、C-style キャストの場合も含めて↑で可搬性がないとしていたコードが可搬性があることになりました。世の人々も指す位置が変わらないと思って reinterpret_cast を使ってるし実装も他に選択肢がなくまず間違いなくそうなってるし、という理由で規定が変更されています(DR658)。もともとコードの見た目でキャストの位置づけが分かるように、という意図で C++ キャストが分けられていたはずなのですが、まぁ現実には勝てない、というところなのでしょうか。

さて、これを B*, C* の例に適用すると、クラスについては、メンバ・基本クラスに非 standard-layout class がないこと、メンバに参照なし、仮想関数・仮想基本クラスなし、継承階層中非静的メンバをもつクラスは自分自身を含めて高々一つ、が standard layout class となるため、B は standard-layout type ですが、C は standard-layout type ではありません。結果、reinterpret_cast についてはやっぱり何も言えない、ということになります。実際には C++03, C++11 いずれの実装であっても指す位置が変わらない、というのが普通の実装でしょう(指す位置を変える積極的な理由がない)。ということを示そうとしたのが http://ccf.myhome.cx:5000/result/12 です。いずれの処理系においても static_cast では指す位置が変わる場合があり、reinterpret_cast では指す位置が変わっていません。なおこのコードでは reinterpret_cast によるポインタと整数との変換をしています。これ自体も(整数のサイズが十分であれば)一周回れば元に戻る、以外はどのような変換が実施されるかは処理系依存です(14882:2003, 14882:2011 5.2.10p5)(そしてアドレスをそのまま整数値とする実装が多いでしょうし、このコードは少なくとも変換が単射であることを期待したコードですので厳密には可搬性はありません)。個人的にはこのポインタと整数の相互変換が C++03 以前で reinterpret_cast を使うべき唯一のケースだと思っています。

さて、「キャストの復習」と題して、static_cast、reinterpret_cast によるポインタの変換について、C++11 での変更を含めてお送りしました。今後 C++ コミュニティにおいて reinterpret_cast の位置づけがどのようになるのか(一部の異なるポインタ型の変換について正当な手段とされるのか、あくまでも処理系依存や未規定なキャストについてのみ使うべきとされるのか)、興味深いところではあります。

まとめ

C++ Advent Calendar 2012 8日目として、「C++ Compiler Farm の紹介」と「キャストの復習」をお送りしました。C++ Advent Calendar 2012 明日の担当は @Flast_ROさんです。お楽しみに→【にゃははー】

0 件のコメント:

コメントを投稿