お久しぶりの田原です。
時が経つのは速いもので第26回を書いてからもう3週間も経過していました。気持ち的には先週書いたばかりです。歳を取ると加速するとは聞きますが、本当に加速しますね。
さて、前回に引続き今回も条件演算子(三項演算子)の話題です。Twitterでいなむ先生から、「ところでvoidは?」って突っ込みを頂きました。その一連の流れから便利な使い方に気がついたのでそれをご紹介します。
1.まえがき
実はrubyという言語ではif文ではなくif式だそうです。というかifだけでなくwhileなどの制御構造も全て「式」だそうです。昔、C言語が登場する前はサブルーチンと関数があり、サブルーチンは値を返しませんでした。C言語でばっさりサブルーチンを切り捨てて、関数が値を返しても無視できたり、void型を返す 関数が定義されたりして、びっくりしたことを思い出します。
void(空虚)を返すとか、まるで0個のりんごのような衝撃です。FORTRANに慣れた身(1以上の自然数に慣れた人のようなものです)にとってかなり衝撃的でした。
rubyは更にそれを推し進めて式にできるものは全部式にしたって感じですね。ちょっと羨ましいです。
C++でコードを書いていて、文を式として書きたいことってたまにありますから。
さて、C/C++でも「if」ならば式があります。そう条件演算子ですね。
2.どんな時if式が欲しいのか?
次のようなコードを書くことってないでしょうか? 私は鈍くさいなと感じつつよく書きます。
std::string str; if (cond) { str=u8"条件成立!!"; }
コンストラクト後に代入の流れがどうも鈍くさいと感じます。最初のコンストラクト時に設定できればスマートなのにと思うのです。また、この後strを変更する必要がないなら、std::string const str;としたい人も居るでしょう。そのような時にif式が便利です。
std::string const str = (cond)?u8"条件成立!!":"";
3.voidと条件演算子
さて、ちょっと話が変わります。式の中にvoidを返す項を書くことはあまりないと思います。
#include <iostream> void foo() {} int main() { std::cout << 1+foo(); }
当たり前ですが、int型とvoid型の足し算はないのでエラーになります。
msvc :'+': void 型のオペランドを持っています。 gcc :invalid operands of types 'int' and 'void' to binary 'operator+' clang:invalid operands to binary expression ('int' and 'void')
ですが、条件演算子は後ろの2項が共にvoidとなることを許します。
#include <iostream> void foo() { std::cout << "foo()\n"; } void bar() { std::cout << "foo()\n"; } int main() { bool cond=false; (cond)? foo() : bar(); }
でも、普通にif文で書けるので使う場面はいまいち分かりません。(こっちの方が読みやすいですし。)
#include <iostream> void foo() { std::cout << "foo()\n"; } void bar() { std::cout << "foo()\n"; } int main() { bool cond=false; if (cond) foo(); else bar(); }
そして、後ろの2項がvoidとvoid以外になることは許されない筈です。
#include <iostream> void foo() { std::cout << "foo()\n"; } int main() { bool cond=false; (cond)? foo() : 123; }
msvc :コンパイル成功 gcc :second operand to the conditional operator is of type 'void', but the third operand is neither a throw-expression nor of type 'void' clang:left operand to ? is void, but right operand is of type 'int'
三者三様ですね。とは言えmsvcでも変数に代入すると当然エラーになります。condがfalseの時、voidを代入しようとするので当たり前なのです。
ところで、gccがちょっと気になるメッセージを出しています。「3番目がthrow式でもvoidでもない」と指摘しています。つまり、throwを書いても良いということでしょうか?
throwって私は文と思っていましたが、文を式の中に書くことはできません。つまり、どうもthrowは式のようです。ということは型がある筈です。調べたらvoid型でした!!
#include <iostream> #include "typename.h" int main() { std::cout << TYPENAME(decltype(throw 123)) << "\n"; }
void
そこで、もう少し調べていたらcppreference.com のnoteに記載がありました。(N3337も見てみたのですが、該当する記述を見つけることができませんでした。膨大なので探しきれなかっただけと思いますが。)
The throw-expression is classified as prvalue expression of type void. Like any other expression, it may be a sub-expression in another expression, most commonly in the conditional operator:
double f(double d) { return d > 1e7 ? throw std::overflow_error("too big") : d; } int main() { try { std::cout << f(1e10) << '\n'; } catch (const std::overflow_error& e) { std::cout << e.what() << '\n'; } }
「throw式はvoid型の右辺値である。他の式と同様他の式の部分式になることができ、主に条件演算子で使われる。」だそうです。
この例では、(bool)?void:double
のような型になってます。なるほど、式の中にも書けるようthrowは式であると定義したということのようです。そして、それが主に使われる条件演算子では2項と3項の片方にthrowを置いた場合はもう一方がvoidでなくても特別に許しているようです。
4.2のif式を少し便利に使ってみます
条件演算子のthenとelseには式しかかけません。つまり、文を書かないと出来ない処理を書けないのです。多くの処理は式で書けますが、僅かに届かなくて悔しいことがあります。もちろん関数にして呼び出せば可能ですが、数行程度の小さなコードならワザワザ関数定義しないで済ませたいです。そんな時はラムダ式ですね!!
#include <iostream> #include <deque> #include <string_view> #include <exception> int main(int argc, char *argv[]) { auto const aArgs = (1 < argc)? [&](){ std::deque<std::string_view> temp; for (int i=1; i < argc; ++i) temp.emplace_back(argv[i]); return temp; }() : throw std::runtime_error("parameter error!!\n"); for (auto arg : aArgs) { std::cout << arg << "\n"; } }
ぐはっ。見慣れないせいかも知れませんが、このサンプル・コード読みにくいです。10~16行目がthen処理用のラムダ式ですが、目がチカチカします。条件演算子とラムダ式の組み合わせはなかなか凶悪です。
NRVOに期待
条件演算子のelse側で値を返さずに例外を投げているので、NRVOが機能する処理系が多いことを期待しています。(gccとclangは機能しました。Visual C++ 2017では残念ながらtempとaArgsのアドレスが異なっていましたので機能していないようです。)ところでstd::string_view
C++17で追加された一種の文字列クラスです。多くのライブラリで、string_ref、StringRefのような命名をされることが多いものです。文字列自体は保持せず、文字列への参照を保持してます。このサンプルのように引数文字列を取り扱いたいけど、コピーする必要がない場合に便利です。
賛否両論あると思いますが、マクロで見栄えを良くしてみます。
#include <iostream> #include <deque> #include <string_view> #include <exception> #define IF(dCond) (dCond)?[&]() #define ELSE_THROW ():throw #define RESULT return int main(int argc, char *argv[]) { auto const aArgs = IF(1 < argc) { std::deque<std::string_view> temp; for (int i=1; i < argc; ++i) temp.emplace_back(argv[i]); RESULT temp; } ELSE_THROW std::runtime_error("parameter error!!\n"); for (auto arg : aArgs) { std::cout << arg << "\n"; } }
更に、次のような工夫も有りと思います。
#include <iostream> #include <deque> #include <string_view> #include <exception> #define IF(dCond) (dCond)?[&]() #define ELSE ():[&]() #define ENDIF () #define RESULT return int main(int argc, char *argv[]) { auto const aArgs = IF(1 < argc) { std::deque<std::string_view> temp; for (int i=1; i < argc; ++i) temp.emplace_back(argv[i]); std::cout << "&temp =" << &temp << "\n"; RESULT temp; } ELSE { throw std::runtime_error("parameter error!!\n"); RESULT std::deque<std::string_view>(); } ENDIF; std::cout << "&aArgs=" << &aArgs << "\n"; for (auto arg : aArgs) { std::cout << arg << "\n"; } }
IF~ELSE~ENDIFは見慣れているので、私自身はこちらの方が好きです。
そして、びっくりしたのですがgccとclangではNRVOが機能するようです。残念ながらmsvcはダメでしたけど。
5.まとめ
見栄えの悪さに日の当たることが少ない(と個人的に感じる)条件演算子にちょっと拘ってみました。
特に2.の使い方は便利と思います。最初に初期化後、可能な時は一気にコンストラクトしてしまいたいですから。ただ、ちょっとばかり見づらいのでマクロで工夫してみました。でも、文中で使ったような短いマクロ名だと被る可能性が高いです。マクロにも名前空間があればいいのにとか思ってしまいます。
さて、C#連携の方も鋭意開発中です。大幅変更するこの機会はデータ・フォーマットの下位互換を切り捨てる良いチャンスなので、C#連携をスムーズに進めることができそうなフォーマットへ変更中です。概ねそれが完了しましたのでいよいC++クラスをC#クラスへ自動変換するよメタ・シリアラザの開発に着手します。
しかし、MinGWは頭が痛いです。 linuxとWindowsの間で苦労している感がひしひしと伝わってきます。