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以前との互換性の維持のためです。将来的には自動生成しないルールになると思われるので、現実のコンパイラの対応がどうであれ、自動生成されないと思っておいた方が良いでしょう。 VisualC++ 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 (戻り値の最適化)

次のプログラムを見て下さい。

class MyClass {};

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

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

このプログラムの実行の過程で起こることを考えると、普通に見れば、

  1. f() の中で一時オブジェクトを生成するため、実引数が 123 のコンストラクタを呼ぶ。
  2. c を生成するとき、一時オブジェクトをコピー(またはムーブ)する。コピーコンストラクタ(またはムーブコンストラクタ)が呼ばれる。
  3. main() の最初の文の終わりで、一時オブジェクトのデストラクタが呼ばれる。
  4. main() の終わりで、c のデストラクタが呼ばれる。

しかし、実際にコンパイラが生成するコードは恐らく、次のように書いた場合と同じになります。

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

試しに、VisualC++ で、MyClassクラスのコンストラクタ、コピーコンストラクタ、デストラクタにログ出力のコードを仕込んで実行してみます。

#include <iostream>

class MyClass {
public:
    explicit MyClass(int value) :
        mValue(value)
    {
        std::cout << "constructor" << std::endl;
    }

    MyClass(const MyClass&)
    {
        std::cout << "copy constructor" << std::endl;
    }

    ~MyClass()
    {
        std::cout << "destructor" << std::endl;
    }

    inline int GetValue() const
    {
        return mValue;
    }

private:
    int  mValue;
};


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

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

実行結果:

constructor
123
destructor

実行結果から分かるように、オブジェクトが2つ(一時オブジェクトと c)作られた様子はありません。これは、RVO (戻り値の最適化) という最適化が働いた結果です。RVO によって、戻り値を返すための一時オブジェクトを生成せず、受け取り側のメモリ領域へ直接オブジェクトを生成する形に置き換えられます。

RVO は、C++ に古くから存在しているよく知られた最適化手法であり、現代のほぼすべてのコンパイラが対応していると思われます。関数内で定義されたオブジェクトをそのまま戻り値とする場合には、ほぼ確実に RVO が行われると期待できます。

C++17 (右辺値による初期化の最適化)

C++17 では、右辺値を使った変数が初期化されるとき、コピーやムーブを省略することが求められるようになりました。つまり、RVO の実装は必須事項となり、確実に最適化されます(一時オブジェクトは右辺値です)。

コピー代入演算子

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

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/1/5 Xcode 8.3.3 を clang 5.0.0 に置き換え。

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

'2017/8/16 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ