C++編【言語解説】 第16章 コピー操作と参照

この章の概要

この章の概要です。

コピー

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);  // ポインタで渡す方がコストが小さい
}

ところで、オブジェクトをコピーする場合、デフォルトの挙動としては、C言語の構造体と同様にすべてのメンバ変数が1つ1つコピーされます。 これは、メンバ変数が「公開」されていないとしても関係なく行われます。
「デフォルトの挙動としては」と書きましたが、実は C++ では、コピーの際の挙動を変更できます。 これについては、後で解説します

参照

先ほどの例のように、ポインタを利用することで、関数呼び出しのコストを減らすことができますが、 C++ の場合は、参照(リファレンス)という機能を使うこともできます。

void func(const MyClass& a)
{
	a.Print();  // a->Print(); ではない
}

int main()
{
	MyClass b;
	func(b);  // func関数の仮引数は参照
}

参照は、型名に & を付けることで表現されます。

参照は言葉通り、何か別のものを参照する機能ですが、 実際のところは、ある既存のオブジェクトに対して、新しい名前を宣言する機能と捉えることができます。 例えば、

int a = 0;
int& b = a;

このコード片の場合、変数a に対して、新しい名前 b を宣言したと考えられます。 このように定義された変数b は、参照変数と呼ばれます。 変数a の別名として b を宣言したと考えられることから、参照変数のことをエイリアス(別名)と呼ぶこともあります。

ここで、b を使って次のように書けば、

b = 10;
std::cout << a << std::endl;  // 10 が出力される

これは 10 を出力します。 つまり、参照 b を使った操作は、参照先の変数 a を使うのと同じであるということです。

このような機能ですから、メモリ上の位置を指し示すポインタとは異なり、 参照には、何も参照していない状態ということはあり得ません。 ですから、未初期化の参照変数を作ることはできません。
また、参照変数は、後から別のものを参照するように書き換えることもできません。

int a;
int b = 10;
int& c;      // 未初期化の参照変数はエラー
int& d = a;  // 参照先が未初期化なのは構わない

d = b;       // 参照先を変更することはできない

参照先が変更できないため、ポインタ変数のように、+、-、++、--、+=、-= といった演算子を使って、 参照位置を移動させることもできません。

ポインタにおける間接参照 (*p のような) は、特別に何か演算子を用いずとも行えます。 そのため、->演算子も、参照を使う際には登場しません。

代入演算子

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

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

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

関数名として、「operator=」という特殊な名称を用いることで、=演算子の処理を定義できます(operator と = の間にはスペースがあっても構いません)。
実はこのように、独自の処理を定義できるのは =演算子だけではなく、ほとんどの演算子で可能です。 このように、演算子の処理を定義することを、演算子オーバーロードと呼びます。 他の演算子での例は、第19章で取り上げます。

自身と同じクラス型のオブジェクトの代入(コピー)を受け付けるには、 このサンプルコードのように、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 (自動生成される代入演算子の明示)

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

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

この機能は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。

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

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

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

この機能は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。

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


operator= を実装する際には、もう1つ注意すべき点があります。 それは、「a = a;」のような使われ方をしても問題が無いようにすることです。 つまり、自分自身へ自分をコピーするような使い方で、自己代入と呼ばれる行為です。
先ほどの実装例のままでも問題はありませんが、普通、自己代入では何も起きないことが望ましい挙動ですから、 最初に、自己代入になっていないかどうかをチェックします。

MyClass& MyClass::operator=(const MyClass& rhs)
{
	if (this != &ths) {  // 自己代入でないときだけ、以下の処理を行う
		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;
};

この機能は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。

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

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

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

この機能は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。


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

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」で削除してしまう方が、意図が明確になり、より良いでしょう。


練習問題

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

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
}


解答ページはこちら

参考リンク

更新履歴

'2017/3/25 VisualC++ 2017 に対応。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2016/7/16 サンプルプログラムに余計な文字が混ざっていたのを修正。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/18 VisualC++2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/18 clang 3.2 に対応。

'2014/7/12 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ