実践C++応用講座こんにちは。お久しぶりの田原です。長い間書かないと、永遠に書かなくなりそうで、ちょっと気を引き締めます(笑)さて、今回は3項演算子(条件演算子)の型について解説します。このブログをお読みの皆さんは既に条件演算子についてはご存知と思います。bとcの型が異なる場合の振る舞いまでご存知の方は少ないのではないでしょうか?かく言う私もつい先日まで知りませんでした。実はこの記事へのいなむ先生のコメントのコードを解読中に初めてしりました。このコードには超絶的なテクニックがつかわれています。その中で使われている条件演算子の使い方についても解説します。1.まずは復習から「条件演算子」は3つの項を取るC/C++唯一の演算子です。条件項、then項、else項の3つです。(*1)a?b:cと書いた時、aが条件項、bがthen項、cがelse項です。aがtrueなら3項演算子の結果はbとなり、falseならcとなります。cには「式」を書くことができます。(*1)then項、else項これらは私の造語です。見かけることはないです。「then項」や「else項」でググッたら結構適切なページが表示されてびっくり。最近、グーグル先生の凄さによく驚かされます。"\n";}条件演算子の優先順位は非常に低いので、多くの場合()で括らないと意図通りに振る舞いませんので括っています。35「変数」や「左辺値参照を返す式」などの左辺値を指定することもできます。"\n";}1234562.型が違う時どうなる?then項とelse項には、同じ型となる式を書くことが多いと思いますが、偶に異なる型を書く誘惑に駆られることがあります。残念ながら、多く場合は挫折してしまいます。^「?:の型が違う!」と怒られました。(?:は条件演算子のことです。)しかし、通るものもあります。"\n";}123さて、ここで問題です。この(true?123:456.7)の型はなんでしょう?結果も123ですし、普通に123を書いた定数はint型ですから、当然int型ですよね??確認してみましょう。Wandboxで確認する。3.型が異なる時のお約束でも、安心して下さい。実は比較的簡単なルールで型が決まります。先程の(true?123:456.7)がdouble型になったのは、上記の3-1.のルールのおかげですね。等の演算子と同じですから大丈夫ですよね?3-2.はちょっとイメージし辛いと思いますので例示します。volatile*3-3.のそれ以外の時というのは、クラスの場合です。クラスの場合、以下の場合に暗黙の型変換が可能です。非explicitな1引数のコンストラクタの型から暗黙の型変換可能}Wandboxで確認する。条件演算子で使ってみましょう。}FooFoointintBarBarWandboxで確認する。上記の変換は全て片方向のみできるものでした。これに、非explicitなint型を1つ引数に取るコンストラクタを加えるとint型とBar型は両方向に暗黙の型変換ができるようになります。^9行目に着目して下さい。「各々の型をもう一方へ変換できる(両方向へ変換できる)」からエラーと書いてあります。Wandboxで確認する。^変換が発生したため、条件演算子の結果は右辺値になりました。右辺値に代入しようとしてエラーになっています。4.超絶技巧いなむ先生のこのソースを見て下さい。次のマクロの中で条件演算子の驚くべき使い方をしています。::cranberries::protean_bool{})まず、マクロの継続行の\がウザいので、それを消しつつインデントを調整してみました。18行目は関数スタイルキャスト(*2)です。bool(条件演算子)となってます。4行目が条件演算子の条件項です。falseですので、結果は必ずelse項となります。else項は17行目のprotean_bool{}です。リンク先のソースを見て下さい。protean_boolにはstd::true_typeとstd::false_typeへのキャスト演算子が定義されています。then項はmake_overload関数テンプレートです。条件項がfalseですのでこの関数が呼ばれることはありません。条件演算子の結果の型を判定するためにコンパイルされます。リンク先のソースを確認下さい。make_overloadはそのテンプレート・パラメータで指定した型を次々と基底クラスとして定義したOverloadクラステンプレートを返却しています。2つ目のラムダ式の型>に実体化されます。各ラムダ式の型はoperator()が定義されたクラスです。上記Overloadクラスはそれらを継承していますので各ラムダ式のoperator()が継承され、全てのoperator()があります。イメージ的には次のようになっています。}};このoperator()の引数に、CRANBERRIES_HAS_TYPEの15行目でtype_placeholder<CLASS>{}を渡しています。これは上記Overloadクラス5行目の仮引数xに渡ります。decltype(x)::type::XXX>()で展開されます。このdecltype(x)は、xの型ですからtype_placeholder<CLASS>です。従って、decltype(x)::typeはtype_placeholder<CLASS>::typeですから、これはCLASSに展開されます。decltype(x)::type::XXXはCLASS::XXXに展開されます。value_type)の時、CLASS::XXXはstd::true_type::value_typeへ展開されます。std::true_typeがメンバの型value_typeを持っていたら、1つ目のジェネリック・ラムダのoperator()が実体化され、これにマッチします。このラムダ式の戻り値はst::true_typeです。ここから条件演算子の性質をびっくりな使い方をしています。上述のラムダ式が返却したstd::ture_typeは元の条件演算子のthen項です。冒頭に書いたように、条件演算子のelse項のprotean_boolはstd::true_typeとstd::false_typeへのキャスト演算子を持っているため、どちらの型へも変換できます。then項はstd::true_typeですから、この条件演算子の結果の型もstd::true_typeになります。条件項がfalseですからprotean_boolを返却しますので、これがstd::true_typeへキャストされたものが、この条件演算子全体の戻り値です。これを最も外側のCスタイルキャストbool()でキャストします。std::true_typeはboolへキャストするとtrueになります。value_type)はstd::true_typeがvalue_typeメンバ型(std::true_typeのなかでtypedefされたもの)を持っていたらtrueになります。x)にマッチしないため、operator()(...)にマッチします。operator()(...)はstd::false_typeを返却するので、上記手順でCRANBERRIES_HAS_TYPE()はfalseになるという仕掛けです。CRANBERRIES_HAS_TYPEはメンバの有無を判定できるという仕掛けです。(*2)関数スタイルキャストCスタイルキャストと呼ばれるC言語時代からあるキャストは「(型)式」と書きます。このCスタイルキャストとほぼ同等の機能を持つ「関数スタイルキャスト」がC++に追加されていました。「型(式)」と書きます。C言語のキャストと同じく問答無用で型変換するのでコンパイラのバグ検出が機能しないこと、および、危険な型変換している箇所を見つけにくくなるためなるべく避けた方が良さそうです。C++でわざわざ追加された機能ですので、何か理由がありそうなので調べてみました。本家stackoverflowに質問がありました。この議論を見る限り、1複数の引数を取るコンストラクタによる型変換と21つの引数を取るコンストラクタによる型変換と3基本型の型変換の3種類を同じ構文で書けるというのがメリットっぽいです。可変長引数テンプレートと共に使うような場合に、関数スタイルキャストを使うとスマートに記述できるケースがありそうです。因みに、CRANBERRIES_HAS_TYPEが非常に慎重な設計が行われていることに議論の余地はありません。このようなケースでstatic_castと書くのかどうかはプログラマの考え方次第と感じます。(短く書けるのもメリットの1つです。)5.まとめ今回は普段あまり気にすることがない条件演算子のthen項とelse項の型が違う時の不思議な振る舞いと、その振る舞いを積極的に利用しているCRANBERRIES_HAS_TYPEについて解説してみました。CRANBERRIES_HAS_TYPEの条件演算子の使い方に着目して解説したので、他の深い部分についてはちょっとかっとばしすぎたような気がします。若干発展(複数のテンプレート引数を持つクラス・テンプレート対応)したバージョンについていなむ先生による解説がQiitaにありますので参照下さい。自分ではこんな超絶技巧のプログラムをとても作れません。このようなものを思いつ人ってすご過ぎです。いつもなら次週予告なのですが、不定期開講なので次回予定は決まっていません。2~3周間の内に次を書きますので待ってて下さい。Castと呼ばれています。Castではありません。