こんにちは。お久しぶりの田原です。
あまり長い間書かないと、永遠に書かなくなりそうで、ちょっと気を引き締めます(笑)
さて、今回は3項演算子(条件演算子)の型について解説します。このブログをお読みの皆さんは既に条件演算子についてはご存知と思います。しかし、(a)?b:c で、bとcの型が異なる場合の振る舞いまでご存知の方は少ないのではないでしょうか?
かく言う私もつい先日まで知りませんでした。実はこの記事へのいなむ先生のコメントのコードを解読中に初めてしりました。このコードには超絶的なテクニックがつかわれています。その中で使われている条件演算子の使い方についても解説します。
1.まずは復習から
「条件演算子」は3つの項を取るC/C++唯一の演算子です。条件項、then項、else項の3つです。(*1)
a?b:c
と書いた時、aが条件項、bがthen項、cがelse項です。aがtrueなら3項演算子の結果はbとなり、falseならcとなります。a, b, cには「式」を書くことができます。
(*1)then項、else項
これらは私の造語です。あまり見かけることはないです。しかし、「then項」や「else項」でググッたら結構適切なページが表示されてびっくり。最近、グーグル先生の凄さによく驚かされます。
#include <iostream> int main() { int x=1; int y=2; int z=3; std::cout << (true ?x+y:y+z) << "\n"; std::cout << (false?x+y:y+z) << "\n"; }
条件演算子の優先順位は非常に低いので、多くの場合()で括らないと意図通りに振る舞いませんので括っています。
3 5
「変数」や「左辺値参照を返す式」などの左辺値を指定することもできます。
#include <iostream> #include <vector> int main() { std::vector<int> vec{0}; int data; (true ?data:vec[0])=123; (false?data:vec[0])=456; std::cout << data << "\n"; std::cout << vec[0] << "\n"; }
123 456
2.型が違う時どうなる?
then項とelse項には、同じ型となる式を書くことが多いと思いますが、偶に異なる型を書く誘惑に駆られることがあります。残念ながら、多く場合は挫折してしまいます。
#include <iostream> int main() { std::cout << (true?123:"abc") << "\n"; }
prog.cc: In function 'int main()': prog.cc:5:23: error: operands to ?: have different types 'int' and 'const char*' std::cout << (true?123:"abc") << "\n"; ^
「?:の型が違う!」と怒られました。(?:
は条件演算子のことです。)
しかし、通るものもあります。
#include <iostream> int main() { std::cout << (true?123:456.7) << "\n"; }
123
さて、ここで問題です。この(true?123:456.7)
の型はなんでしょう?
結果も123ですし、普通に123を書いた定数はint型ですから、当然int型ですよね??
確認してみましょう。
#include <iostream> #include "typename.h" int main() { std::cout << (true?123:456.7) << "\n"; std::cout << TYPENAME(true?123:456.7) << "\n"; }
123 double
えっ! doubleになりました! びっくりです。
Wandboxで確認する。
3.型が異なる時のお約束
でも、安心して下さい。実は比較的簡単なルールで型が決まります。
- then項からelse項へ暗黙の型変換可能な時はelse項の型
- else項からthen項へ暗黙の型変換可能な時はthen項の型
- 両方向とも変換出来る時は、
3-1. 数値型の場合は表現範囲の広い方
3-2. ポインタ型の場合は、合成
3-3. それ以外の時はエラー - 左辺値を指定した場合でも、変換されたら右辺値
先程の(true?123:456.7)
がdouble型になったのは、上記の3-1.のルールのおかげですね。
このルールは普通の + や – 等の演算子と同じですから大丈夫ですよね?
3-2.はちょっとイメージし辛いと思いますので例示します。
#include <iostream> #include "typename.h" int main() { int const* cp = nullptr; int volatile* vp = nullptr; std::cout << TYPENAME(true?cp:vp) << "\n"; }
int const volatile*
3-3.のそれ以外の時というのは、クラスの場合です。
クラスの場合、以下の場合に暗黙の型変換が可能です。
– 基底クラスがある
– キャスト演算子が定義されている
– 非explicitな1引数のコンストラクタがある
#include <iostream> struct Foo { }; struct Bar : public Foo // 基底クラスがある { Bar() { } // キャスト演算子 operator int() { return 123; } // 非explicitな1引数のコンストラクタ Bar(char const*) { } }; int main() { Foo foo; Bar bar; int data; foo = bar; // 基底クラスへ暗黙の型変換可能 data = bar; // キャスト演算子がある型へ暗黙の型変換可能 std::cout << data; bar = "abc"; // 非explicitな1引数のコンストラクタの型から暗黙の型変換可能 }
では、条件演算子で使ってみましょう。
#include <iostream> #include "typename.h" struct Foo { }; struct Bar : public Foo // 基底クラスがある { Bar() { } // キャスト演算子 operator int() { return 123; } // 非explicitな1引数のコンストラクタ Bar(char const*) { } }; int main() { Foo foo; Bar bar; int data; // 基底クラスへ暗黙の型変換可能 std::cout << TYPENAME(true ?foo:bar) << "\n"; std::cout << TYPENAME(false?foo:bar) << "\n"; // キャスト演算子がある型へ暗黙の型変換可能 std::cout << TYPENAME(true ?data:bar) << "\n"; std::cout << TYPENAME(false?data:bar) << "\n"; // 非explicitな1引数のコンストラクタの型から暗黙の型変換可能 std::cout << TYPENAME(true ?bar:"abc") << "\n"; std::cout << TYPENAME(false?bar:"abc") << "\n"; }
Foo Foo int int Bar Bar
上記の変換は全て片方向のみできるものでした。
これに、非explicitなint型を1つ引数に取るコンストラクタを加えるとint型とBar型は両方向に暗黙の型変換ができるようになります。
#include <iostream> #include "typename.h" struct Bar { Bar() { } // キャスト演算子 operator int() { return 123; } // 非explicitな1引数のコンストラクタ Bar(int) { } }; int main() { Bar bar; int data; // 両方向に型変換できるとコンパイルエラー std::cout << TYPENAME(true ?data:bar) << "\n"; }
In file included from prog.cc:2:0: prog.cc: In function 'int main()': prog.cc:21:32: error: operands to ?: have different types 'int' and 'Bar' std::cout << TYPENAME(true ?data:bar) << "\n"; ^ prog.cc:21:18: note: in expansion of macro 'TYPENAME' std::cout << TYPENAME(true ?data:bar) << "\n"; ^ prog.cc:21:32: note: and each type can be converted to the other std::cout << TYPENAME(true ?data:bar) << "\n"; ^ prog.cc:21:18: note: in expansion of macro 'TYPENAME' std::cout << TYPENAME(true ?data:bar) << "\n"; ^
9行目に着目して下さい。「各々の型をもう一方へ変換できる(両方向へ変換できる)」からエラーと書いてあります。
Wandboxで確認する。
そして、最後に4.の例です。
#include <iostream> int main() { short short_data; int int_data; (true ?short_data:int_data)=123; }
prog.cc: In function 'int main()': prog.cc:8:32: error: lvalue required as left operand of assignment (true ?short_data:int_data)=123; ^
変換が発生したため、条件演算子の結果は右辺値になりました。右辺値に代入しようとしてエラーになっています。
4.超絶技巧
いなむ先生のこのソースを見て下さい。
次のマクロの中で条件演算子の驚くべき使い方をしています。
#define CRANBERRIES_HAS_TYPE(CLASS, XXX) \ bool(false \ ? ::cranberries::make_overload( \ [](auto x)->decltype(std::declval<typename decltype(x)::type::XXX>(), std::true_type{}) {return{}; }, \ [](...)->std::false_type {return{}; } \ )(::cranberries::type_placeholder<CLASS>{}) \ : ::cranberries::protean_bool{})
まず、マクロの継続行の\
がウザいので、それを消しつつインデントを調整してみました。
#define CRANBERRIES_HAS_TYPE(CLASS, XXX) bool ( false? ::cranberries::make_overload ( [](auto x)-> decltype ( std::declval<typename decltype(x)::type::XXX>(), std::true_type{} ) {return{}; }, [](...)->std::false_type {return{}; } )(::cranberries::type_placeholder<CLASS>{}) : ::cranberries::protean_bool{} )
2, 3, 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クラステンプレートを返却しています。
それはOverload<1つ目のジェネリック・ラムダの型, 2つ目のラムダ式の型>
に実体化されます。
そして、各ラムダ式の型はoperator()が定義されたクラスです。上記Overloadクラスはそれらを継承していますので各ラムダ式のoperator()が継承され、全てのoperator()があります。つまり、イメージ的には次のようになっています。
struct Overload { // 1つ目のジェネリック・ラムダのoperator() tempalte<typename T> auto operator()(T x)-> decltype ( std::declval<typename decltype(x)::type::XXX>(), std::true_type{} ) { return std::true_type{}; } // 2つ目のラムダ式のoperator() auto operator()(...) -> std::false_type { return std::false_type{}; } };
このoperator()の引数に、CRANBERRIES_HAS_TYPEの15行目でtype_placeholder<CLASS>{}
を渡しています。
これは上記Overloadクラス5行目の仮引数xに渡ります。
これが、8行目のstd::declval<typename decltype(x)::type::XXX>()
で展開されます。
このdecltype(x)
は、xの型ですからtype_placeholder<CLASS>
です。
従って、decltype(x)::type
はtype_placeholder<CLASS>::type
ですから、これはCLASSに展開されます。
そして更に、decltype(x)::type::XXX
はCLASS::XXX
に展開されます。
さて、CLASSとXXXはCRANBERRIES_HAS_TYPEマクロの引数ですから、
CRANBERRIES_HAS_TYPE(std::true_type, 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になります。
以上の結果、CRANBERRIES_HAS_TYPE(std::true_type, value_type)はstd::true_typeがvalue_typeメンバ型(std::true_typeのなかでtypedefされたもの)を持っていたらtrueになります。
逆に持っていなければ(実際には持ってますが)、operator()のオーバーロード解決時にoperator()(T x)にマッチしないため、operator()(…)にマッチします。
operator()(…)はstd::false_typeを返却するので、上記手順でCRANBERRIES_HAS_TYPE()はfalseになるという仕掛けです。
かくてCRANBERRIES_HAS_TYPEはメンバの有無を判定できるという仕掛けです。
(*2)関数スタイルキャスト
Cスタイルキャストと呼ばれるC言語時代からあるキャストは「(型)式」と書きます。そして、このCスタイルキャストとほぼ同等の機能を持つ「関数スタイルキャスト」がC++に追加されていました。「型(式)」と書きます。C言語のキャストと同じく問答無用で型変換するのでコンパイラのバグ検出が機能しないこと、および、危険な型変換している箇所を見つけにくくなるためなるべく避けた方が良さそうです。
しかし、C++でわざわざ追加された機能ですので、何か理由がありそうなので調べてみました。
本家stackoverflowに質問がありました。
この議論を見る限り、①複数の引数を取るコンストラクタによる型変換と②1つの引数を取るコンストラクタによる型変換と③基本型の型変換の3種類を同じ構文で書けるというのがメリットっぽいです。可変長引数テンプレートと共に使うような場合に、関数スタイルキャストを使うとスマートに記述できるケースがありそうです。因みに、CRANBERRIES_HAS_TYPEが非常に慎重な設計が行われていることに議論の余地はありません。このようなケースでstatic_castと書くのかどうかはプログラマの考え方次第と感じます。(短く書けるのもメリットの1つです。)
5.まとめ
今回は普段あまり気にすることがない条件演算子のthen項とelse項の型が違う時の不思議な振る舞いと、その振る舞いを積極的に利用しているCRANBERRIES_HAS_TYPEについて解説してみました。
ただ、CRANBERRIES_HAS_TYPEの条件演算子の使い方に着目して解説したので、他の深い部分についてはちょっとかっとばしすぎたような気がします。若干発展(複数のテンプレート引数を持つクラス・テンプレート対応)したバージョンについていなむ先生による解説がQiitaにありますので参照下さい。
しかし、凄いですね。自分ではこんな超絶技巧のプログラムをとても作れません。このようなものを思いつ人ってすご過ぎです。
さて、いつもなら次週予告なのですが、不定期開講なので次回予定は決まっていません。
また2~3周間の内に次を書きますので待ってて下さい。よろしく!!
Type( expr )
または
Type{ expr }
というシンタックスのキャストは
Function Style Cast
と呼ばれています。
C Style Castではありません。
うわっ。本当ですね。機能的にはC言語のキャストと事実上同じですが、C++で新たに追加された構文ですね。
記事を訂正します。ご指摘ありがとうございました。