Modern C++編【言語解説】 第13章 コピー

先頭へ戻る

この章の概要

この章の概要です。


コピーコンストラクタ

あるクラスのオブジェクトのコピーを作るとき、以下のように記述できます。

MyClass b = a;

この記述を行ったときに呼び出されているコンストラクタは、コピーコンストラクタと呼ばれています。コピーコンストラクタは、次のように定義します。

class MyClass {
public:
    // 通常のコンストラクタ
    explicit MyClass(int value) :
        mValue(value)
    {}

    // コピーコンストラクタ
    MyClass(const MyClass& rhs) :
        mValue(rhs.mValue)
    {}

private:
    int  mValue;
};

コピーコンストラクタは通常、自身のクラス型の const参照を引数に取ります。仮引数の rhs という名前は「right-hand side(右辺)」のことで、これといった的確な名前が無いときによく使われています。

const の付かない参照や、volatile を使うことも許可されていますが、そういった書き方を使うことはほぼありません。

「MyClass(const MyClass& rhs, int option = 0);」のように、後続の仮引数にデフォルト実引数がある場合は、それを無視すれば「MyClass(const MyClass& rhs);」とみなせるので、コピーコンストラクタとして機能します。

コピーコンストラクタの実装の基本は、引数で受け取ったコピー元のオブジェクトのメンバ変数を1つ1つコピーする形になります。しかし例えば、メンバ変数にポインタが含まれている場合、ポインタがコピーされるのではなく、ポインタが指し示す先にあるものを含めてコピーすることが適切なこともあります。これはディープコピーと呼ばれる処理です。こういう場合は、自分でコピー処理を記述する必要があります。ディープコピーについては、後で改めて取り上げます

次のような、通常のコンストラクタを使ってインスタンス化してから、代入(コピー)を行う形は非効率なので、コピーコンストラクタを使うようにして下さい。

MyClass a(10);  // 初期化
MyClass b(20);  // 初期化
b = a;          // 代入

これが非効率なのは、初期化(コンストラクタの実行)と、代入(コピー)をそれぞれ行ってしまうためです。コピーコンストラクタならば、この2つを1つにまとめられますから、効率的です。

コピーコンストラクタは、コンパイラが自動生成することがあります。このルールは、C++11 で追加された新機能の影響を受けて、少しややこしくなっていて、まとめると次のようになります。

  1. 明示的にコピーコンストラクタを実装しなければ、自動生成される。
  2. ただし、ムーブコンストラクタ(第14章)やムーブ代入演算子(第14章)を実装しているときは、自動生成されない。
  3. 明示的にコピー代入演算子(本章)やデストラクタを実装しているときは、自動生成を推奨しない。

3つ目のルールに関しては、自動生成しないことをコンパイラに推奨しているということです。これは C++03以前との互換性の維持のためです。将来的には自動生成しないルールになると思われるので、現実のコンパイラの対応がどうであれ、自動生成されないと思っておいた方が良いでしょう。 VisualStudio 2017 や clang 5.0.0 では、コピー代入演算子やデストラクタを明示的に実装していても、コピーコンストラクタが自動生成されます。

コンパイラが自動生成するコピーコンストラクタは、すべてのメンバ変数をコピーするだけの単純な実装です。この実装で問題無ければ、明示的に実装する必要はありません。コピーコンストラクタの存在をコード上で明らかにするために、コンパイラと同じ実装を記述するくらいならば「=default」を使うようにして下さい。

class MyClass {
public:
    MyClass(const MyClass&) = default
};

一時オブジェクト

一時オブジェクト(テンポラリオブジェクト)とは、ソースコード上には現れない、名前の無いオブジェクトのことです。名前が無いので、これは右辺値です。 一時オブジェクトは、コンパイラの判断によって、自動的に生成・破棄するコードが埋め込まれます。

一時オブジェクトが作られる代表的な場面は、関数がオブジェクトを返す場合です。ここでいう「オブジェクト」には、int型や double型といった基本的な型や、構造体型、クラス型などが含まれています。次のプログラムを見て下さい。

#include <iostream>

class MyClass {
public:
    explicit MyClass(int value) :
        mValue(value)
    {}

    inline int GetValue() const
    {
        return mValue;
    }

private:
    int  mValue;
};


MyClass f()
{
    MyClass c(123);
    return c;
}

int main()
{
    MyClass c = f();
    std::cout << c.GetValue() << std::endl;
}

実行結果:

123

ここからの話は、コンパイラが行う最適化を無視しています。この場面で起きる最適化について、後の項で取り上げています

f関数の戻り値は MyClass型なので、ポインタや参照ではなく、実体のあるオブジェクトです。そのため、f関数のローカル変数 c のコピーを作って返却します。ここで作られるコピーが、一時オブジェクトです。ちなみに次のように書いても同じ意味になります。

MyClass f()
{
    return MyClass(123);
}

名前を付けずに「型名()」と書く構文があり、これで一時オブジェクトを明示的に作れます。( ) はコンストラクタの呼び出しなので、実引数を書くことができます。

一時オブジェクトは、生成された場所を含んだ完全式の終わりのタイミングで破棄されることになっています。完全式というのは、他の式の一部になっていない式のことを指します。
サンプルプログラムで言うと、一時オブジェクトが作られたのは、f関数を呼び出す式(関数呼び出し式)の中になりますが、これは「MyClass c = f()」という式の一部です。結局のところ、「MyClass c = f()」の実行が完了したタイミングが、一時オブジェクトが破棄されるタイミングとなります。破棄される前に、変数c へコピーしていますから、一時オブジェクトが破棄されても問題ありません。

RVO (戻り値の最適化)

ここまでに取り上げた関数では、戻り値が実体であることが少し気になるかもしれません。前章では、引数の型を参照(あるいはポインタ)にすることで、関数呼び出しのコストを低減できるという話題がありましたが、戻り値の場合は、こういった方法が取れません。静的でないローカル変数を指すポインタや参照を返してしまうと、関数を抜け出した時点で指し示す先の変数が消えてしまうため、不正アクセスを起こす恐れがあるからです。

実際には、実体のまま戻り値を返しても、効率的なコードが生成される可能性が高いです。何が起きるのでしょうか?

次のプログラムを使って、オブジェクトの生成や破棄がどれだけ行われるのかを調べてみます。

#include <iostream>

class C {
public:
    C()         { std::cout << "constructor" << std::endl; }
    C(const C&) { std::cout << "copy constructor" << std::endl; }
    ~C()        { std::cout << "destructor" << std::endl; }
};

C func()
{
    return C();
}

int main()
{
    C c = func();
}

素直に処理を追いかけて考えると、以下の4つの呼び出しが起こるように思えます。

  1. func関数の戻り値にする一時オブジェクト C() を生成するコンストラクタ
  2. 戻り値を使って、c を生成するコピーコンストラクタ
  3. 戻り値を破棄するデストラクタ
  4. s を破棄するデストラクタ

しかし実際に試すと、以下の結果を得られるでしょう。

実行結果:

constructor
destructor

つまり、コピーコンストラクタの呼び出しが省かれ、その分、デストラクタも1回分減っているということです。省略されたコピーコンストラクタとは、一時オブジェクトを生成する際のもので、これを省略した代わりに、戻り値を利用する呼び出し側の方へ直接、オブジェクトを生成します。こうして、余分なコピーを省き、効率を向上します。

これは、コンパイラが行う最適化で、RVO (Return Value Optimization: 戻り値の最適化) と呼ばれている手法です。この最適化がすべてのコンパイラで行われる保証はありませんが、まず間違いなく行われるといえるほど一般的なものです。

後述しますが、C++17 では保証されます。

VisualStudio での Debugビルド時のように、この最適化が起こらないことはあり得ます。

この最適化で呼び出されなくなったコピーコンストラクタとデストラクタには、標準出力への出力という処理が含まれていることにも注目して下さい。つまり、何か副作用を持った処理が含まれていたとしても、容赦なく省略しています。普通、コンパイラが行う最適化は、意味が変わらないように行われるものですから、これは非常に特殊です。

プログラムを次のように変形すると、結果が変わるかもしれません。

#include <iostream>

class C {
public:
    C()         { std::cout << "constructor" << std::endl; }
    C(const C&) { std::cout << "copy constructor" << std::endl; }
    ~C()        { std::cout << "destructor" << std::endl; }
};

C func()
{
    C c;
    return c;
}

int main()
{
    C c = func();
}

VisualStudio 2015 の Debugビルドで試すと、次の結果を得られます。

実行結果:

constructor
copy constructor
destructor
destructor

何も最適化されていないようです。

前のサンプルプログラムでは一時オブジェクトを返していましたが、今回は名前のあるオブジェクトである点に違いがあります。とはいえ実際のところ、このサンプルプログラムの形であっても、前のサンプルプログラムと同じ理屈で最適化を行うコンパイラが多くあります。例えば、clang は最適化を行って、次の実行結果を出力します。

実行結果:

constructor
destructor

最適化の理屈は同じで、コピーを省略し、その結果、デストラクタの呼び出しも減ります。前の最適化と同じ結果になるので、これも RVO と呼ぶことがありますし、一時オブジェクトではなく名前を持ったオブジェクトであることを強調して、NRVO (Named Return Value Optimization: 名前付き戻り値の最適化) と呼び分けることもあります。

NRVO を働かせるためには、ローカルオブジェクトの型が関数の戻り値型と同じであることと、return文に与える式が、そのローカルオブジェクトの名前だけであることを守らなければなりません。ただし、関数の仮引数を return する場合には NRVO は働きません。

C++17 (RVO の保証)

C++17 では、コピーやムーブ(第14章)の省略を行うことができる幾つかのケースのうちの1つについて、省略が行われることが保証されるようになりました。

コピー(またはムーブ)の省略が保証されるケースは、参照で束縛していない一時オブジェクトを、同じ型(const、volatile の有無は無視していい)のオブジェクトへコピー(またはムーブ)する場合です。これは、初期の C++規格の頃から行われ得る最適化ですが、C++17 に至るまでは保証がありませんでした。

そして、このケースが、関数からの return の際に起こることがすなわち RVO なので、C++17 では RVO も保証されます。一時オブジェクトでなければならないため、NRVO は依然として保証されません


コピー代入演算子

オブジェクトのコピーを行う際、そのクラス用に定義されているコピー代入演算子が使用されます。単に、代入演算子と呼ぶ場合もあります。

C++11 から、コピー代入演算子と同じ「=」という記号を使った、ムーブ代入演算子が追加されたため、区別を付けるために "コピー" という名称を入れています。ムーブ代入演算子は、第14章で説明します。

C++ では演算子の動作を変更する機能があり、コピー代入演算子の動作も変更できます。

class MyClass {
public:
    MyClass(const MyClass& rhs);
    MyClass& operator=(const MyClass& rhs);
    
private:
    int mValue;
};

MyClass::MyClass(const MyClass& rhs) :
    mValue(rhs.mValue)
{
}

MyClass& MyClass::operator=(const MyClass& rhs)
{
    // メンバ変数をコピー
    mValue = rhs.mValue;
    return *this;
}

演算子の動作を変更するには、「operator 演算子」という名前の特殊な関数を定義します。「operator」と「演算子」の間の空白はあっても無くても構いません。演算子全般について動作を変更する話題は、第20章で改めて取り上げます。本章では、コピー代入演算子に限った説明を行います。なお、演算子の動作を変更する機能を、演算子オーバーロードと言います。

operator=() を定義しておくと、「a = b」のような代入式で operator=() が呼び出されます。このとき、右辺の内容が実引数となり、戻り値が左辺側に返されます。ちなみに「a = b」は「a.operator=(b)」と書くのと同じことで、普通はしませんが、後者の書き方でもコンパイル可能です。

また、operator=() を定義したのであれば、コピーコンストラクタも定義するのが普通です。どちらもコピーなので、一方の処理だけを書き換えるようなことは問題があります。例えば「MyClass a = b;」と「a = b;」とで結果が異なるのはおかしいでしょう。ただし、コピーコンストラクタを定義すると、デフォルトコンストラクタが自動的には生成されなくなることに注意して下さい。

ここでは、operator=() の引数は、自身のクラスの const参照型にしています。同じクラスのオブジェクトのコピーを行うのであればこの指定が適切です。他の型からの代入を受け付けるのであれば、それに合わせた引数を持った operator=() を定義できます。

int型を受け取り、int型を返すような operator=() を定義することも可能ですが、operator=() を持つクラスと異なる型を扱うことは、コピーとは言えないため好ましくはありません。

operator=() の戻り値は、自身のクラスの左辺値参照にして、*this を返すように実装するのが基本です。単なる this はポインタなので、間接参照を行った結果の参照にします。こうすることで、次のような連続的な代入が可能になります。

MyClass a, b, c;
a = b = c;

これは、「a.operator=(b.operator=(c));」と同じことです。これをよく見ると、「b.operator=(c)」の戻り値(b の参照)が「a.operator=()」の実引数になることが分かります。

もし戻り値を左辺値参照ではなく、ポインタで実装しようとすると、代入式が以下のような不自然な形になってしまいます。

class MyClass {
public:
    MyClass* operator=(const MyClass* rhs)    {
        mValue = rhs->mValue;
        return this;
    }
    
private:
    int mValue;
};

MyClass a, b;
a = &b;  // ?

まるで、ポインタ変数 a に b のメモリアドレスを代入しているように見えてしまいます。このような不自然なコードを避けつつ、実体をコピーするコストも避けることが、参照という機能が追加されたそもそもの理由です。


コピーコンストラクタと同様、コピー代入演算子は、コンパイラが自動生成することがあります。やはり、ルールはややこしいですが、以下のようになっています。

  1. 明示的にコピー代入演算子を実装しなければ、自動生成される。
  2. ただし、ムーブコンストラクタ(第14章)やムーブ代入演算子(第14章)を実装しているときは、自動生成されない。
  3. 明示的にコピーコンストラクタ(本章)やデストラクタを実装しているときは、自動生成を推奨しない。

3つ目のルールに関しては、自動生成しないことをコンパイラに推奨しているということです。これは C++03以前との互換性の維持のためです。将来的には自動生成しないルールになると思われるので、現実のコンパイラの対応がどうであれ、自動生成されないと思っておいた方が良いでしょう。 VisualC++ 2017 や clang 5.0.0 では、コピーコンストラクタやデストラクタを明示的に実装していても、コピー代入演算子が自動生成されます。

自己代入

operator=() を実装する際には、「a = a;」のような使われ方をしても問題が無いかどうかに注意して下さい。このような自分自身へ自分をコピーするような使い方は、自己代入と呼ばれます。

普通、自己代入は、少なくとも見た目の上では何も起きないことが望ましい挙動です。例えば、次のような場合は、自己代入になってもコピー代入の処理は行われていますが、見た目の上では何も起きていないように見えます。

class MyClass {
public:
    MyClass(const MyClass& rhs) :
        mValue(rhs.mValue)
    {}

    MyClass& operator=(const MyClass& rhs)
    {
        mValue = rhs.mValue;
        return *this;
    }

private:
    int mValue;
};

この場合、自分の mValue に、自分の mValue をコピーするだけなので、代入の処理を行った側から見ると、変化が無いように見えます。実際にはコピー処理が行われているので、多少の無駄はありますが、それ以外には問題がありません。

無駄にコピーされることを防ぐために、operator=() の冒頭部分で自己代入かどうかをチェックして、コピーを省く方法もありますが、チェック自体にもコストが掛かることも踏まえると、あまり効果的とは言えないかもしれません。

MyClass& operator=(const MyClass& rhs)
{
    if (this != &rhs) {
        mValue = rhs.mValue;
    }
    return *this;
}
また、例外(第18章)への備えを考えると、別の手法を取り入れた方が良いケースが多いでしょう。この辺りの話題は、例外について解説するときに改めて取り上げます。

ディープコピー

プログラマが自分で operator=() を定義する場面としては、例えば、メンバ変数に動的に確保された領域を指すポインタ変数が含まれているケースがあります。

#include <cstdlib>
#include <cstring>

class Name {
public:
    Name(const char* name)
    {
        mName = static_cast<char*>(std::malloc(std::strlen(name) + 1));
        std::strcpy(mName, name);
    }

    ~Name()
    {
        std::free(mName);
    }

private:
    char*  mName;
};

int main()
{
    Name name1 = "Ken";
    Name name2 = "John";
    name2 = name1;
}

まだ解説していないため、ここでは std::malloc関数や std::free関数を使用していますが、C++ では new演算子や delete演算子を使うべきです。これらは第15章で説明します。また、文字列の場合であれば、標準ライブラリの std::string を使う方がより良いです。こちらは【標準ライブラリ】第10章で解説します。

コンパイラが自動的に生成する operator=() は、メンバ変数をコピーするだけのシンプルなものです。つまり、次のような形になります。

Name& operator=(const Name& rhs)
{
    mName = rhs.mName;
    return *this;
}

mName は std::malloc関数によって動的確保された領域を指すポインタで、デストラクタのところで std::free関数によって解放されています。上記のような operator=() の実装では、同じ領域を指すポインタが2つできることになってしまい、name1、name2 という2つのオブジェクトのデストラクタそれぞれで、同じ領域を解放しようとします。

このようにポインタ変数を含んでいるときに、ポインタ変数自体をコピーするだけの挙動は、シャローコピー(浅いコピー)と呼ばれています。シャローコピーでは、動的な領域を指すポインタ変数が混ざっていると、致命的な問題につながります。

そこで回避策として、ポインタ変数が指し示している先にある領域もコピーするという方法が考えられます。ポインタ変数自体は、コピーされた新しい領域を指すようにします。このような挙動のコピーは、ディープコピー(深いコピー)と呼びます。

Name& operator=(const Name& rhs)
{
    // 新しい領域を作り、内容をコピー
    char* const n = static_cast<char*>(std::malloc(std::strlen(rhs.mName) + 1));
    std::strcpy(n, rhs.mName);

    // 以前の領域を解放
    std::free(mName);

    // 新しい領域を指すようにポインタを付け替える
    mName = n;

    return *this;
}

処理の順序に注意が必要です。まず、新しい領域を作るようにします。動的確保はメモリ不足等で失敗することがあるため、以前の領域の解放を先に行ってしまうと、情報を失ってしまう可能性があります。このような考え方は、例外(第18章)に備えたプログラムを書く際に重要になりますが、より良い方法もあります。この辺りの話題は、例外について解説するときに改めて取り上げます。

また、operator=() を定義したのならば、コピーコンストラクタも同じように実装すべきですが、当然同じようなコードになります。以下のように「非公開」なメンバ関数を作って、共通化することはできます。

class Name {
public:
    Name(const Name& rhs) : mName(nullptr)
    {
        Copy(rhs);
    }

    Name& operator=(const Name& rhs)
    {
        Copy(rhs);
        return *this;
    }
    
private:
    void Copy(const Name& rhs)
    {
        // 新しい領域を作り、内容をコピー
        char* const n = static_cast<char*>(std::malloc(std::strlen(rhs.mName) + 1));
        std::strcpy(n, rhs.mName);

        // 以前の領域を解放
        std::free(mName);

        // 新しい領域を指すようにポインタを付け替える
        mName = n;
    }
    
private:
    char*  mName;
};

現時点の知識で出来るのは、このように1か所にコードをまとめることです。この場合、コピーコンストラクタ内でも mName に対する std::free関数の呼び出しが行われるため、事前に mName がヌルポインタになるように初期化しておく必要があります。

前述した通り、例外について解説するときに改めて取り上げますが、これとは異なる解決策があります。考え方だけ書いておくと、operator=() の中でローカルなオブジェクトを、コピーコンストラクタを使って作り、そのオブジェクトと *this のオブジェクトとを入れ替え(swap) すれば良いです。こうすると安全かつ、コードの重複も無くなります。

コピーを禁止する

クラスによっては、オブジェクトがコピーできない方が都合が良いこともあります。そのような場合は、コピーを作り出す2つの方法、つまり、コピーコンストラクタとコピー代入演算子を使用できないようにすれば良いです(当然、コピーと同等の処理を行うメンバ関数が無いことを前提としています)。

C++03以前は、コピーコンストラクタとコピー代入演算子を「非公開」にする方法が使われていましたが、C++11 以降なら「=delete」を使って関数を削除するのが良いです。これは、第10章でも取り上げた機能です。

#include <cstdlib>
#include <cstring>

class Name {
public:
    Name(const char* name)
    {
        mName = static_cast<char*>(std::malloc(std::strlen(name) + 1));
        std::strcpy(mName, name);
    }

    ~Name()
    {
        std::free(mName);
    }
    
    Name(const Name&) = delete;
    Name& operator=(const Name&) = delete;

private:
    char*  mName;
};

int main()
{
    Name name1 = "Ken";
    Name name2 = name1; // コンパイルエラー
    name2 = name1;      // コンパイルエラー
}


練習問題

問題① 次のプログラムのコメント部分では何が行われているかを、コンストラクタ、コピーコンストラクタ、コピー代入演算子、デストラクタといった関数のどれが呼び出されているのかという観点から説明して下さい。

class MyClass {
};

MyClass func1(MyClass mc)
{
    return mc;
}

MyClass* func2(MyClass* mc)
{
    return mc;
}

MyClass& func3(MyClass& mc)
{
    return mc;
}

int main()
{
    MyClass a;       // A
    MyClass b = a;   // B
    MyClass c(b);    // C
    MyClass* d;      // D

    c = a;           // E

    c = func1(a);    // F
    d = func2(&a);   // G
    c = func3(a);    // H
}


解答ページはこちら

参考リンク



更新履歴

'2018/7/17 「RVO (戻り値の最適化)」の項を、同じ内容を説明した C++編(【言語解説】第17章)の変更に合わせて修正。C++17 での保証についても内容を改めた。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/1/5 Xcode 8.3.3 を clang 5.0.0 に置き換え。

'2017/8/22 C++11 以降の動作に合わせて、コピーや一時オブジェクトに関する記述を追記・修正。

'2017/8/16 新規作成。



前の章へ(第12章 参照)

次の章へ(第14章 右辺値参照とムーブ)

Modern C++編のトップページへ

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー