C/C++ プログラミング

C++11| コピー初期化(A a = A()みたいなやつ)の罠

例えば次のようなシンプルなクラスを考えます。

    class A {
      public:
          A() {}  // デフォルトコンストラクタ
    };
    // インスタンス化
    A a = A();

この時、どのメソッドがどのように呼びだされるのか実際の動作をトレースしてみます。

コード上に記述されていませんが C++ では暗黙に自動生成されるメンバ関数があることに留意する必要があります。上のコードではコピーコンストラクタと代入演算子、それに C++11 以降であればムーブコンストラクタとムーブ代入演算子が暗黙に宣言されます。

さて、インスタンス化の行:

A a = A();

は”=”の記号で代入するかのような表記ですけども、文法上はコピーコンストラクタによる初期化文の形式のひとつになります。

参考:
プログラミング言語C++第4版』16.2.2 デフォルトのコピー(456p)

動作を確認するために以下のように実装してみます。

test.cpp:
#include <iostream>
#include <string>

int main(void)
{
    class A {
      public:
        // (1)デフォルトコンストラクタ
        A()
        {
            std::cout << "- default constructor -" << std::endl;
        }
        // (2)コピーコンストラクタ
        A(const A&)
        {
            std::cout << "- copy constructor -" << std::endl;
        }
        // (3)代入演算子
        A& operator=(const A&) = delete;
        void print()
        {
            std::cout << "TEST PRINT" << std::endl;
        }
    };
    
    A a = A();
    a.print();
    
    return  0;
}

「(3)代入演算子」が削除(delete宣言)されたこのコードは正しくコンパイルが通り、したがってこのコードにおいて代入演算子が不要なのが文法的に正しい事がわかります。

そしてコンパイルしたプログラムを実行してみると、標準出力は

$ ./test.exe
- default constructor -
TEST PRINT

のようになり、いきなりデフォルトコンストラクタによる初期化となっていてコピーコンストラクタは呼ばれません。呼ばれないなら不要なのだろうかと「(2)コピーコンストラクタ」をコメントアウトし、delete 宣言してコンパイルしようとすると…

#if 0
        A(const A&)
        {
           std::cout << "- copy constructor -" << std::endl;
        }
#endif
        A(const A&) = delete;

…すると、今度は"A(const A&)が呼べないよ!"という感じのコンパイルエラーになります(エラー文言はコンパイラにより異なる)。つまり、文法的にはコピーコンストラクタを呼ぶ建前になってるわけです。

※コピーコンストラクタの代りにムーブコンストラクタを定義してもコンパイルが通ります。

まとめ

    A a = A();

の形式の場合コピーコンストラクタを呼ぶ前提で文法チェックされるが実際にはコピーコンストラクタは呼ばれない(場合がある)。なので『この形式の初期化ではコピーコンストラクタが呼ばれるはずだからそこで何がしかの処理をいれよう』などとやると罠にハマる可能性があります(そのような場面は想定しにくいし、良い作法とも思えませんが…)

今回の記事では cygwin64/x86_64-w64-mingw32 g++ 5.4.0 で動作を確認しましたが、この挙動は g++ における最適化の結果であると考えられます。ざっくりした説明としては"コピーコンストラクタはオブジェクトをコピーするものなので、デフォルトコンストラクタで生成したものをコピーするならばデフォルトコンストラクタのみ呼び出すだけでよい"という理屈ですが、今回の例のようにログ文言などの副作用や「敢えて違う内容のオブジェクトを生成するケース」は無視されてしまいます。通常の最適化では副作用まで消去してしまうとコンパイラのバグ扱いですが、今回のケースは例外的にC++の言語仕様で許可されている実装であってほかのコンパイラでも同じ挙動になる可能性は高いので注意です。より掘り下げた議論は"RVO C++"のキーワードでググると色々でてきます。

LINE B!

コメントを残す

サイト管理人が承認後にコメントが表示されます。
※コメント内容のみ必須入力です。
メールアドレスはサイト管理人のみに通達されます。