最終更新日時(UTC):
が更新

履歴 編集

厳密な式の評価順(C++17)

概要

C++14までは式の評価順序が未規定(unspecified)であったが、 C++17では次の式は全てa, bの順で評価されるように定義された。

  1. a.b
  2. a->b
  3. a->*b
  4. a(b1, b2, b3)
  5. b @= a
  6. a[b]
  7. a << b
  8. a >> b

関数の引数リスト内の式(上記例で言えばb1, b2, b3)の評価順序は未規定である。

仕様

C++17では式の評価順序が下記の通りに定められた。

  • 左から右に評価される。
    • メンバへのポインタ演算子 (.*, ->*)
    • 関数呼び出し演算子、およびT(...)による初期化
    • 添え字演算子
    • シフト演算子
  • 右から左に評価される。
    • 代入演算子
    • 複合代入演算子(代入と演算を同時に行う+=, -=, |=などのこと)
  • オーバーロードされた演算子の場合、同様の組み込み演算子の評価順序によって決定される。
    • オーバーロードされた演算子の実際の動作はメンバ関数呼び出しだが、わざとメンバ関数呼び出しの規則を適用しない。
  • new式(メモリ確保を初期化子の評価順序の前に規定した)

3番目の規則については、 代入演算子operator=をオーバーロードした場合を考えるとわかりやすいと思う。 代入演算子は右から左の順、つまりa, bの順で評価される。

struct Hoge {
};

int main()
{
  Hoge a, b;
  b = a; // a, bの順で評価される
}

Hogeクラスの代入演算子をオーバーロードすると、 b = a;b.operator=(a);となりメンバ関数呼び出しと等価になる。 このとき3番目の規則が存在しなかったとすると、メンバ関数呼び出しの規則が適用される。 メンバ関数呼び出しの評価順序は左から右の順、つまりb, aの順で評価され、 代入式の評価順序と逆になってしまう。

struct Hoge {
  Hoge& operator=(const Hoge& a) {
    return *this;
  };
};

int main()
{
  Hoge a, b;
  // b.operator=(a); と等価
  // 仮に3番目の規則が存在しなかったら...
  b = a; // b, aの順で評価される(逆になってしまう!)
}

プログラムの見た目は全く同じ代入式にも関わらず、 代入演算子オーバーロードの有無によって評価順序が逆になってしまう。 このような振る舞いはプログラマを混乱させるだけだろう。 3番目の規則は「演算子オーバーロードにわざとメンバ関数呼び出しの規則を当てはめない」ことで、 演算子オーバーロードの有無に関わらず自然な動作を実現するための規則である。

#include <iostream>
#include <map>

int main() {
  std::map<int, int> m;
  m[0] = m.size(); // C++17 では右から左に評価されるため m は {{0, 0}} になる
  std::cout << m[0] << std::endl;
}

出力

0

この機能が必要になった背景・経緯

C++規格の策定以来、式の評価順序は厳密に定められていなかった。

例に出てきたプログラムは非常に単純だが、 C++14やそれ以前のC++の規格では動作が不定であった。 代入式の評価順序が規定されていなかったためである。

#include <map>

int main() {
  std::map<int, int> m;
  m[0] = m.size(); // C++14 では m が {{0, 0}} か {{0, 1}} のどちらになるか不定
}

下記のプログラムの動作を考える。 std::cout, f, g, hが互いに作用する (例えばf, g, h内でstd::coutに何か出力するなど)場合、 このプログラムの動作は不定であった。

シフト演算子は左結合なので(((std::cout << f) << g) << h)の順に評価されそうだが、 残念ながらそうならない。

std::cout << f() << g() << h();

std::coutはシフト演算子オーバーロードを行っているため、 実際にはメンバ関数の呼び出しとして解決される。

std::cout.operator<<(f()).operator<<(g()).operator<<(h());

このときoperator<<(f()).operator<<(g())のように、 メンバ関数呼び出しの連鎖が発生するが、この評価順は未規定であり、 右から左、すなわちg, fの順で評価されるかも知れないし、 左から右、すなわちf, gの順に評価されるかも知れない。

以上のように、シンプルな代入演算やメンバ関数の連鎖などC++で広く使われている手法でさえ、 動作結果が不定となってしまう問題がある。 この問題はプログラミング作法が悪いのではなく、 C++規格が現代のプログラミング作法に合わなくなっていることが原因である。 C/C++規格の制定から約30年の時を経て、C++17で是正されることとなった。

C++17では全ての式の評価順を厳密に定めることはあえて避けている。 C++の既存のプログラムを壊すこと無く、なおかつ、 広く用いられているプログラミング手法が不定な動作にならないよう、 対象を限定して注意深く変更した結果である。

関連項目

参照