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

先頭へ戻る

この章と同じ(または似た)情報を扱うページが、Modern C++編 (C++11/14/17 対応) の以下の章にあります。

この章の概要

この章の概要です。


コピー

C言語でも、C++ でも、代入演算子を使えば、右辺の式の結果が左辺へコピーされます。

a = b;  // b の内容が a へコピーされる

これは、関数に実引数を渡す際でも同様です。

a(b);  // b の内容が関数a の引数としてコピーされる

コピー処理は、基本的にはコピーする大きさに応じて処理時間が掛かります。この時間を減らすために、C言語では、構造体を関数に渡す際にポインタを利用するといった工夫を行うことが一般的でした(C言語編第33章)。このような事情は C++ で、クラス型のオブジェクトをコピーするときにも当てはまります。

void func(const MyClass* a)
{
    a->Print();
}

int main()
{
    MyClass b;
    func(&b);  // ポインタで渡す方がコストが小さい
}

デフォルトでは、オブジェクトのコピーは、すべてのメンバ変数を1つ1つコピーします。これは、メンバ変数が「公開」されていないとしても関係なく行われます。後で取り上げますが、この挙動は変更することができます。

一時オブジェクト

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

どんなときに一時オブジェクトが作られるのでしょうか。代表的なのは、戻り値として、オブジェクトを返す場合です。なお、ここでいう「オブジェクト」には、int型や double型といった組み込み型や、構造体型、クラス型などが含まれています。

#include <iostream>
#include <string>

std::string func()
{
    return "xyz";
}

int main()
{
    std::cout << func() << std::endl;
}

実行結果:

xyz

func関数の戻り値の型は、std::string なので、ポインタや参照ではなく、実体のあるオブジェクトです。また、実際に返そうとしているのは、"xyz" という文字列リテラルです。つまり、この場面において、名前の付いたオブジェクトは存在していません。

ここでコンパイラは、std::string型の一時オブジェクトを生成し、その初期値として "xyz" を与え、この一時オブジェクトを関数の呼び出し元へ返すようなコードを生成します。これは、次のように書いた場合と同じになります。

std::string func()
{
    return std::string("xyz");
}

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

一時オブジェクトは、生成された場所を含んだ完全式の終わりのタイミングで破棄されることになっています。完全式というのは、他の式の一部になっていない式のことを指します。

今回の例で言うと、一時オブジェクトが作られたのは、func関数を呼び出す式(関数呼び出し式)の中になりますが、これは「std::cout ~ std::endl」という式の一部です。結局のところ、「std::cout ~ std::endl」の実行が完了したタイミングが、一時オブジェクトが破棄されるタイミングとなります。

では、次のように書き換えたらどうなるでしょう?

#include <iostream>
#include <string>

std::string func()
{
    return "xyz";
}

int main()
{
    std::string s = func();
    std::cout << s << std::endl;
}

実行結果:

xyz

この場合も、一時オブジェクトは作られており、今回は完全式の終わりは「std::string s = func()」を終えたところということになります。しかし今回の例では、一時オブジェクトが破棄されてしまう前に、s という変数へコピーしていますから、一時オブジェクトが破棄されても、s へアクセスすることに問題はありません。

更に書き換えてみます。

#include <iostream>
#include <string>

std::string func()
{
    return "xyz";
}

int main()
{
    const char* s = func().c_str();
    std::cout << s << std::endl;
}

実行結果:




今度は、std::string::c_str関数(【標準ライブラリ】第2章)が返すポインタを受け取っています。std::string::c_str関数は、std::string が内部で持っている生の文字列のメモリアドレスを返すため、一時オブジェクトが破棄されてしまうと、一緒に不正なものになってしまいます。この例では、一時オブジェクトは、「const char* s = func().c_str()」を終えたところで破棄されますから、それより後で、変数s が保持しているメモリアドレスを参照するのは不正です。

RVO (戻り値の最適化)

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

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

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

#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: 戻り値の最適化) と呼ばれている手法です。この最適化がすべてのコンパイラで行われる保証はありませんが、まず間違いなく行われるといえるほど一般的なものです。

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 は働かないのが普通です。


代入演算子のオーバーロード

オブジェクトのコピーを行う際、そのクラス用に定義されている代入演算子が使用されることになっています。代入演算子は、次のように自分で定義することが可能です。

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

MyClass& MyClass::operator=(const MyClass& rhs)
{
    // 代入操作時に行う処理を記述
    return *this;
}

関数名として、「operator=」という特殊な名称を用いることで、=演算子の処理を定義できます(operator と = の間にはスペースがあっても構いません)。

実はこのように、独自の処理を定義できるのは =演算子だけではありません。ほとんどの演算子で可能です。このように、演算子の処理を定義することを、演算子オーバーロードと呼びます。他の演算子での例は、第19章で取り上げます。

operator= は、*this を返すように実装して下さい。単に「return this;」だとポインタになってしまうので、「*this」のように間接参照を行ってください。戻り値の型が参照になっているので、自身の参照 (*this) を返しておけば、「a = b = c;」のような連続的な呼び出しが可能になります。これは「a.operator=(b.operator=(c));」と同じです。

自身と同じクラス型のオブジェクトの代入(コピー)を受け付けるには、このサンプルコードのように、operator= の仮引数は「const 自身のクラス名&」、戻り値の型は「自身のクラス名&」として下さい。operator= の仮引数が、自身のクラスの参照型であることによって、同じ型のオブジェクトが代入可能になります。

仮引数を別の型に変えることも可能ではありますが、一般的ではなく、避けた方が良いです。

MyClass a, b;
a = b;  // b が operator= の実引数である

ちょっとイメージしにくいかもしれませんが、実は次のように書くこともできることを知ると意味が分かるかも知れません。

MyClass a, b;
a.operator=(b);

こうして見ると、代入演算子の右辺が、operator= の実引数になることが分かると思います。「operator=」は変な名前ですが、所詮はメンバ関数の名前に過ぎませんから、こういう書き方は可能です(ただし普通、こういう書き方はしません)

ここで注目すべきなのは、前者の代入演算子を使った自然な書き方であっても、後者の operator= を明示的に書く書き方であっても、右辺(または実引数)が「&b」ではなく「b」だということです。コピー処理に掛かる処理時間を減らすため、ポインタを利用するという考え方がありますが、もしポインタを使ってしまうと、次のように書かなければなりません。

MyClass a, b;
a = &b;           // b のメモリアドレスを a に代入?
a.operator=(&b);  // b のメモリアドレスを a に代入?

これは不自然ですし、ポインタ変数にメモリアドレスを代入する場合との区別も付かなくなり混乱しそうです。参照という機能が追加された理由は、こういう箇所において自然な表記を維持しつつ、処理コストを低減することにあります。

なお、コンストラクタやデストラクタと同様に、operator= は、プログラマが自分で定義しなければ、コンパイラが自動的に定義します。自動的に定義された場合は、すべてのメンバ変数が単純に代入されるコードになります。

C++11 (自動生成される代入演算子の明示)

C++11 では、コンパイラが自動生成するメンバ関数を、明示的に記述することが可能です。勿論、代入演算子でも可能です。

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

C++11 (代入演算子の削除)

C++11 では、コンパイラが自動生成する関数を、生成させないように削除できます。勿論、代入演算子でも可能です。

class MyClass {
public:
    MyClass& operator=(const MyClass& rhs) = delete;
};

ディープコピーの実現

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

class OtherClass {
};

class MyClass {
public:
    MyClass(OtherClass* other) :
        mObj(other)
    {}

    ~MyClass()
    {
        delete mObj;
    }

private:
    OtherClass* mObj;  // デフォルトのコピー動作だと、同じものを指すポインタが2つ出来ることになる
};

int main()
{
    MyClass mc1(new OtherClass());
    MyClass mc2(new OtherClass());

    mc2 = mc1;  // mc2.mObj は上書きされてしまう (delete されていない)
}  // mc1, mc2 のデストラクタが呼び出されるが、それぞれ同じ OtherClassオブジェクトを delete しようとする

デフォルトのコピーの挙動だと、上記の例のように、delete が行われることなく上書きされてしまいます。また、同じ領域を指すポインタ変数が2つ出来上がることにも注意が必要です。これは、ポインタ変数が指し示す先をコピーするのではなく、ポインタ変数自体がコピーされている点が問題です。このようなコピーは、シャローコピー(浅いコピー)と言います。

operator= を自分で実装することで、こういった問題を回避できます。

MyClass& MyClass::operator=(const MyClass& rhs)
{
    OtherClass* p = new OtherClass();  // オブジェクトを新規で作る
    *p = *rhs.mObj;        // オブジェクトの内容(メンバ変数)をコピー

    delete mObj;              // コピー先にあったオブジェクトは解放しておく
    mObj = p;

    return *this;             // 自身の参照を返す
}

まず、新規で OtherClassオブジェクトを作り、メンバ変数だけをコピーさせるようにします。ここで、「*p = *rhs.mObj;」は、OtherClassクラスの operator= を呼び出していますが、これは自分で定義していないので、デフォルトの挙動になります。もちろん、OtherClassクラスが MyClassクラスと同じ事情を抱えているのなら、OtherClassクラス用に operator= を定義しなければいけないかもしれません。

その後、コピー先が持っている mObj に対する delete を行います。そして、先ほど作った新しいオブジェクトを代入します。

真っ先にコピー先の mObj を delete するように書いてしまいそうですが、new演算子の実行は失敗する可能性を持っているため(第14章参照)、先に delete してしまうと、コピー元が元々持っていた情報を失ってしまう可能性があります。

このように、ポインタ変数の指し示す先をコピーし、ポインタ変数自体も作り直すように行うコピーは、ディープコピー(深いコピー)と言います。

自己代入

operator= を実装する際には、もう1つ注意すべき点があります。それは、「a = a;」のような使われ方をしても問題が無いようにすることです。つまり、自分自身へ自分をコピーするような使い方で、自己代入と呼ばれる行為です。

普通、自己代入では何も起きないことが望ましい挙動ですから、最初に、自己代入になっていないかどうかをチェックすることが考えられます。前の項でのサンプルプログラムの場合は、このようなチェックをしなくても特に問題はありませんが、例として、自己代入をチェックするように修正してみましょう。

MyClass& MyClass::operator=(const MyClass& rhs)
{
    if (this != &rhs) {  // 自己代入でないときだけ、以下の処理を行う
        OtherClass* p = new OtherClass();  // オブジェクトを新規で作る
        *p = *rhs.mObj;        // オブジェクトの内容(メンバ変数)をコピー

        delete mObj;              // コピー先にあったオブジェクトは解放しておく
        mObj = p;
    }
    return *this;             // 自身の参照を返す
}

自身のメモリアドレス (thisポインタの値) と、引数で渡されたオブジェクトのメモリアドレスとが一致していたら、自己代入です。その場合には、単に *this を return するだけにすれば、自己代入のときには何もしなくなります。

前述した通り、実のところ、このチェックが無くても正しく動作します。このチェックを入れる利点は、自己代入時の無駄が省かれることにありますが、チェック自体にもコストが掛かっているので、自己代入が頻繁に起こらないのであれば、むしろチェックしないという方針も考えられます。

コピーコンストラクタ

あるクラスのオブジェクトをコピーしたいとき、代入演算子を使って、

b = a;

と書けますが、新規のオブジェクトを作ろうとしているのであれば、

MyClass b = a;

このように書く方が自然でしょう。実際、このように書くことはできますが、コンストラクタ周りに関して知っておかなければならないことがあります。

自身と同じクラス型によってオブジェクトを生成する場合には、コピーコンストラクタという特殊なコンストラクタが呼び出されることになっています。コピーコンストラクタは、次のように定義します。

class MyClass {
public:
    MyClass(const MyClass& rhs);

private:
    OtherClass*  mObj;
};

MyClass::MyClass(const MyClass& rhs) :
    mObj(new OtherClass())
{
    *mObj = rhs.mObj;
}

コピーコンストラクタは、代入演算子と同様、自身のクラスの参照型を引数に取ります。const修飾子は、コピーコンストラクタの要件としては必須ではありませんが、通常は付けておくべきです。なお、コンストラクタの一種なので、戻り値はありません。

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

代入演算子の実装と比べると、コピーコンストラクタの実装は割と単純です。決定的な違いとして、代入演算子では、コピー先が元々持っていた情報を削除しなければいけませんが、コピーコンストラクタは、これからオブジェクトを新規作成するところなので、単純に各メンバ変数に適切な初期値を与えていけばいいだけです。

コピーコンストラクタは、自分で書かなければコンパイラが自動的に生成します。その場合の挙動は、デフォルトの代入演算子と同様、すべてのメンバ変数をシャローコピーするというものです。

C++11 (自動生成されるコピーコンストラクタの明示)

代入演算子と同様、コピーコンストラクタも自動生成されますが、C++11 では明示的に記述できます。

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

C++11 (コピーコンストラクタの削除)

代入演算子と同様、コピーコンストラクタも自動生成されますが、C++11 では自動生成させずに削除できます。

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

ここで、以下のコード片を考えてみます。

MyClass a;
a.func();  // a に変更が加わる

MyClass b;
b = a;     // a と同じ状態の b を作る

このような実装は無駄があります。オブジェクト b は、「MyClass b;」の時点で、コンストラクタが呼び出されています。その後、「b = a;」によるコピーが行われるので、コンストラクタで行った初期化は上書きされますから、コンストラクタの呼び出し自体が無意味になります。

そこで、コピーコンストラクタを使って次のように書き替えます。

MyClass a;
a.func();  // a に変更が加わる

MyClass b = a;  // a と同じ状態の b を作る

前の例だと、コンストラクタ+代入演算子という2段構えになっていたのに対し、後の例だと、コピーコンストラクタだけで、オブジェクト b が作られます。この方が無駄が無く効率的です。ちなみにこのとき、

MyClass b(a);  // a と同じ状態の b を作る

このように書くこともできます。この方が効率が良いと説明されている記事もありますが、実際には同じです

「MyClass b = a;」よりも「MyClass b(a);」の方が効率が良い可能性があるのは、a と b の型が異なる場合です。この場合、前者の書き方だと、a の型を b の型に変換する処理を行って一時的なオブジェクトを生成し、その一時オブジェクトをコピーコンストラクタに引き渡すことで実現されることがあります。実際には、コンパイラがコードの最適化を行い、型の変換~一時オブジェクト生成の部分を省略してくれるかもしれませんが、できるだけ、後者の書き方をするようにしておいた方が確実という訳です。

コピーを禁止する

クラスによっては、オブジェクトがコピーできない方が都合が良いケースがあります。ここまで見てきたように、C++ でコピーが起こる場面では、代入演算子かコピーコンストラクタが呼び出されますから、これを呼び出せないように、「非公開」にしてしまえば、コピーはできなくなります。

class MyClass {
public:
    MyClass();  // コピーコンストラクタを明示的に宣言すると、
                // 他のコンストラクタは自動生成されなくなるので、必要なら明示的に書くこと

private:
    MyClass(const MyClass&);
    MyClass& operator=(const MyClass&);
};

このとき、代入演算子、コピーコンストラクタの定義を書く必要はありません。MyClassクラスの他のメンバ関数内からは、「非公開」であってもアクセスできてしまうので、定義を書くと、普通に呼び出せてしまいます。定義を書かなければ、リンクエラーを起こせますから、定義は書かないのが適切です。

また、C++ では、使用することがない仮引数の名前は、付けなくても構いません

非公開継承(第28章)を利用すれば、MyClassクラスの他のメンバ関数からの呼び出しもコンパイルエラーにすることが可能です。詳細な解説は、外部サイト (More C++ Idioms) を参照して下さい。

C++11 であれば、代入演算子とコピーコンストラクタを「= delete」で削除してしまう方が、意図が明確になり、より良いでしょう。


練習問題

問題① ある関数の仮引数が次のようになっているとき、実引数が渡される際に何が行われているか説明して下さい。

void func(std::string s);
void func2(const std::string& s);

問題② 次のプログラムはコンパイルエラーになります。理由を説明して下さい。

#include <string>

void func(std::string& s) {}

int main()
{
    func("abc");
}

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

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/9/14 コピーに関する話題を、第16章から移動。
章のタイトルを変更(「一時オブジェクト」-->「コピー」)
「参照による束縛」の項を削除し、第16章で解説するようにした。
「C++11 (rvalue reference、右辺値参照)」「C++11 (参照修飾子)」の項を削除。

'2018/7/29 「C++11 (rvalue reference、右辺値参照)」「C++11 (参照修飾子)」の項の内容を削除。同じ内容を解説している Modern C++編のページへのリンクだけを残した。

'2018/7/17 「RVO (戻り値の最適化)」の項を全面的に書き直した。

'2018/4/5 VisualStudio 2013 の対応終了。

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

≪更に古い更新履歴を展開する≫



前の章へ(第16章 参照)

次の章へ(第18章 static (静的))

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

Programming Place Plus のトップページへ


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