コピーコンストラクタの省略

以下のような C++ のコードがあります。 実行せずにどのような出力が得られるか予想してみて下さい。

#include <iostream>

struct Foo {
  Foo() { std::cout << "default constructor." << std::endl; }
  Foo(const Foo&) { std::cout << "copy constructor." << std::endl;}
  ~Foo() { std::cout << "destructor." << std::endl; }
};

int main(void) {
  Foo a = Foo();
  std::cout<<"Hello."<<std::endl;
  return 0;
}

素直に考えればこんな順序になります。

  • Foo() はデフォルトコンストラクタを呼出し、一時オブジェクトを生成する。
  • コピーコンストラクタが起動して a を構築する。
  • 完全式の終わりで一時オブジェクトはデストラクタで解体される。
  • Hello. と出力する。
  • a がデストラクタで解体される。

これを踏まえて出力結果を予想するとこうです。

default constructor.
copy constructor.
destructor.
Hello.
destructor.

でも、コンパイラ (処理系) によって、あるいは最適化オプションの違いなどによってこうなる場合もあります。

default constructor.
Hello.
destructor.

いったいどちらが正しいのでしょうか?

ここで規格を参照してみましょう。 JISX3014:2003 の 12.8 にはこうあります。

処理系は、ある基準が満たされれば、そのクラスオブジェクトのコピーによる構築を省略してよい。 これは、このオブジェクトのコピーコンストラクタ及び/又はデストラクタが副作用をもつ場合も含む。 コピーによる構築を省略する場合、処理系は、省略されたコピー演算のコピー元とコピー先を、同一オブジェクトに対する二つの異なる参照として扱い、この最適化を施さなかった場合に二つのオブジェクトが解体される時点の遅いほうで、そのオブジェクトを解体する。

この場合に当て嵌めれば、 a は一時オブジェクトの参照として扱われ、オブジェクトの解体は return のタイミングということになります。 しかし、この省略はあくまでも「してもよい」というものです。 しなくてもよいのです。 つまり、上述のふたつの例はどちらも規格に適合しているのでした。

具体的に省略が許される条件まではここでは述べませんが、オブジェクトを構築する以外の副作用をコピーコンストラクタに持たせている場合には注意が必要だということは覚えておくとよいでしょう。 ちなみに gcc だと -fno-elide-constructors というオプションを付けることによって省略しないように強制することが出来ます。

Document ID: b9984f0a5106171636f432eb81b30193