C++ if文再訪

今回はC++規格書のif文周りを読んでみたい。

今更一体全体何なんだと思う人が大半だと思うが、普段息をするように使っているものだからこそ、規格書を読んでおくのは良いことだと思うのだ。普通の人が規格書を読むのは、知らないものについて勉強するときか、既に知っているものに新機能が追加されたときだろう。初期に勉強した機能ほど、厳密な規格を知らずに使っている気がする。そしてそういう基本的な機能ほど当たり前だがよく使うので影響範囲は広い。となると、規格に当たらずに使っているのがだんだん怖くなってこないだろうか。

というわけで読もう。11, 14, 17の変遷も追いたい。

C++11

まずはC++11(策定直後のドラフト N3337)から見てみる。ifについての記述があるのは、§6.4 "Selection statements"だ。

ここでは、ifswitchのような、複数の制御フローの中から一つだけを選択するための文について書かれている。以下のような形になるらしい。

selection-statement:
    if ( condition ) statement
    if ( condition ) statement else statement
    switch ( condition ) statement
condition:
    expression
    attribute-specifier-seq_opt decl-specifier-seq declarator = initializer-clause
    attribute-specifier-seq_opt decl-specifier-seq declarator braced-init-list

とりあえず、今回はifに集中したいのでswitchは無視する。さらに、ifに続くことのできるstatementの詳細は§6全体で説明されている「文」であり、これもどんなものがあるか一つ一つ見ていったりすると長くなるのでスキップしたい。

ifの次に続く文は暗黙にブロックスコープを導入すると書かれている。つまり、

if (x)
    int i;

は、

if (x) {
    int i;
}

と等価であり、iifを抜けるとスコープを抜けてしまうことが書かれている。ブロックを明示的に書こうが書くまいが、ifに続く変数の寿命はifの外では尽きる。せやな。

さて、conditionは以下のように定義されている。

condition:
    expression
    attribute-specifier-seq_opt decl-specifier-seq declarator = initializer-clause
    attribute-specifier-seq_opt decl-specifier-seq declarator braced-init-list

expressionはよい。これはboolとして評価できるべき式であり、みなさんが普段使っている形式だ。だが他の2つは比較的使われていないというか、知られていないかも知れない。

attribute-specifier-seqは属性構文というやつで、[[nodiscard]]とかそういうやつだ。alignasもこれに含まれるらしい(§7.6.1)。_optは省略可の意。

decl-specifier-seqdecl-specifierの列で、以下のようになっている(§7.1)。

decl-specifier:
    storage-class-specifier
    type-specifier
    function-specifier
    friend
    typedef
    constexpr
decl-specifier-seq:
    decl-specifier attribute-specifier-seq opt
    decl-specifier decl-specifier-seq

順に見てみよう。storage-class-specifierは、register, static, thread_local, extern, mutableのどれかを指す。registerなんてあったなそういえば。registerは値が何度も何度も使われるから効率の良い場所に置いてね、というヒントを与えるためのもので、処理系があまりそういうことが上手でなかった古の時代には意味があったが、現代ではほぼ全てのコンパイラが「言われんでもわかっとるわ」とばかりにガン無視を決め込むキーワードである。覚えなくていい。C++11時点でdeprecatedであり、将来的に全く異なる機能で再利用する方向で議論されている(autoのように)。

type-specifierは……長くなる。詳細は§7.1.6を見て欲しいのだが、まあ要するに型名のことだと思ってくれていい。

function-specifierinlinevirtualexplicitのどれかだ。関数の前につくやつ。「そうですね」って感じ。

ほか、friendtypedefconstexprはもういいだろう。そのままなので。

というわけで、decl-specifierは何かのdeclarationの時に出てくるやつということがわかる。

condition:
    expression
    attribute-specifier-seq_opt decl-specifier-seq declarator = initializer-clause
    attribute-specifier-seq_opt decl-specifier-seq declarator braced-init-list

declarator = initializer-clauseとかdeclarator braced-init-listはもうわかるだろう。x = 1とかx{1, 2, 3}とかのことだ。つまり、if(condition)conditionでは変数宣言ができる。ただし、それはboolに変換可能である必要があり、そうでなければそのプログラムは不適格となる。

ちなみにここで導入された変数はそのif直後の(暗黙かどうかを問わない)ブロックスコープにおいて生存する。ちなみに、そのifの後のelseに続くスコープも同様である。わかりやすい例が規格書に乗っているので引用しておこう。

if (int x = f()) {
  int x; // 不適格、xを再定義している
}
else {
  int x; // 不適格、xを再定義している
}

これはあまり知られていないっぽいくせに結構便利な機能で、例えば以下のようなことができる。

boost::optional<data> get_data();

if(auto data = get_data()) {
    // データ処理
}

とか、

boost::expected<data, error> parse();
if(auto parsed = parse()) {
    // データ処理
} else {
    // エラー処理
}

などだ。モダンな言語でよく採用されている、エラー処理を直和型を使って明示的にやっていく方式を採用するなら、この構文はかなり役に立つと思う。

boost::optionalは使い方の説明で当たり前のようにこの構文を使っている。既にこの機能を知っているという人はboost::optionalのドキュメントで知ったとかではないだろうか。他に紹介している文献を寡聞にして見たことがない。ちなみに私はboost::optional経由で知った。

C++14

ではC++14で何か変わったか調べるために、規格策定直後のドラフト、N4140を見てみよう。構成があまり変わっていないので、同じ§6.4にある。読んでみよう。

変化、なし――

まあ、C++14はマイナーチェンジだったから仕方ないね。

C++17

C++17では結構大きめの変更が入っている。規格書の構成も結構変わって、該当する箇所が§9.4になった。

selection-statement:
    if constexpr_opt ( init-statement_opt condition ) statement
    if constexpr_opt ( init-statement_opt condition ) statement else statement
    switch ( init-statement_opt condition ) statement

if constexprも同じ所で触れるんだな。まあこれに関しては日本語でも結構解説があるので一旦飛ばすことにしよう。普通のifに注目する。

init-statementはあってもなくても構わないが、ある場合はセミコロンで続くconditionと区切られる。このinit-statementはその名の通り何かの変数をその場で定義できるというもので、以下のような使い方ができる。

if(auto x = calc(); x > 0.0) {
    // ...
} else {
    // ...
}

ではこの初期化式で導入された変数の生存期間はどうなるかというと、§9.4.1に以下のような記述がある。

if constexpr_opt (init-statement condition) statement

は、以下のコードと等価である。

{
    init-statement
    if constexpr_opt (condition) statement
}

というわけで、if全体をブロックスコープで覆ったと思えばよい。ifのスコープを離れた時点で寿命は突き、デストラクタが呼ばれる。

constexpr ifの話をしなかったら割とあっさり終わってしまった。

まとめ

たかがif、と思っていても、実はあまり見る機会のない構文が使えることがある。逆にifのような最初期に学ぶであろう機能こそ、そういった知られざる便利機能が隠れていたりするのではないか。

規格書にあたるのは大事ですね。