ひょんなことからC++をやり始めたので,備忘録がてら確認した機能,確認のために書いたコードを書いていく.
概要
C++では以下のようなコードを書く時,コンストラクタ,コピーコンストラクタ,代入演算子が暗黙的に実行される
class A {}
int main() {
A a1, a3; // (1)コンストラクタの実行
A a2 = a1; // (2)コピーコンストラクタの実行
a3 = a1; // (3)代入演算子の実行
}
(1)は,クラスAの変数の宣言に伴い,引数なしのコンストラクタが実行される.(2)は,クラスAの変数a2が変数宣言と同時に代入演算子によってa1に初期化される.このように,初期化される時コピーコンストラクタが呼ばれる(後述するメソッドの引数,返り値も同様).(3)は初期化済み変数への代入で,このとき代入演算子が実行される.
これを踏まえ,Aを拡張しながら動作を確認していく.
コピーコンストラクタ
コピーコンストラクタの書式は,
クラス名 (const &obj)
のように定義する.
A(const A &obj){}
これを踏まえ,以下のようにクラスAを拡張する
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "Constructor A" << endl;}
A(const A &obj) {
cout << "copy Constructor A" << endl;
}
};
コピーコンストラクタは,以下の3つのケースで呼び出される.
- 変数の初期化
- メソッドに与える引数
- メソッドからの返り値
よって,以下のコードで実験する
int main() {
A a1;
A a2 = a1;
}
実行結果
>g++ A.cpp
>./a.out
Constructor A
copy Constructor A
a2
の初期化でコピーコンストラクタが実行されていることがわかる.
次に,メソッドの引数と返り値についても確認する.以下のテストコードを実行する
A getA(A obj) {
return obj;
}
int main() {
A a1;
A a2 = a1;
a1 = getA(a1);
}
実行結果
Constructor A
copy Constructor A
copy Constructor A
copy Constructor A
コピーコンストラクタが3回実行されている.よって,getA
の呼び出し時の引数,および返り値それぞれでコピーコンストラクタが実行されていることがわかる.続いて,以下のコードでも実験してみる
A getA2() {
A a;
return a;
}
int main() {
A a1;
a1 = getA2();
}
実行結果
Constructor A
Constructor A
これは予想に反する.本来,2回のコンストラクタ呼び出しの後,return a
でコピーコンストラクタが実行されるはずである.
また,以下のようにすると,
A getA2() {
cout << "getA2" << endl;
A a;
return a;
}
int main() {
A a1 = getA2();
}
実行結果は
getA2
Constructor A
となる.今回もコピーコンストラクラは呼ばれない.どうやら,これはコンパイラの最適化が影響している模様.
よって,A a1 = getA2();
では本来コピーコンストラクタが2回実行されるはず(getA2の戻り値とa1の初期化)だが,恐らくインライン展開されてただの代入が実行されているように見える.そこで,これを確かめるべく,代入演算子の実装も追加する.
代入演算子
代入演算子の書式は,
type operator = ( const &obj )
のように定義する.
なお,代入演算子は一般には演算子のオーバーロードであり,その一般的な書式は
type operator operator-symbol ( parameter-list )
である.
ここでは,以下のように定義する
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "Constructor A" << endl;}
A(const A &obj) {
cout << "copy Constructor A" << endl;
}
A& operator = (const A& obj) {
cout << "insert" << endl;
return *this;
}
};
なお,この定義についての詳細は後述する.
この状態で再度以下のコードを実行する
A getA2() {
A a;
return a;
}
int main() {
A a1;
a1 = getA2();
}
すると,以下の実行結果を得る
Constructor A
getA2
Constructor A
insert
最後”insert”と表示されていることから,コピーコンストラクタではなく,代入演算子が実行されていることがわかる.
この結果,最適化によってメソッドがインライン展開されているのだと予想できる.
ちなみに,こうしても以下の実行結果は
A getA2() {
cout << "getA2" << endl;
A a;
return a;
}
int main() {
A a1 = getA2();
}
getA2
Constructor A
であったことから,先のコードは
int main() {
A a1;
}
のように最適化されたのだと思う.
代入演算子の再定義について
このクラスでは,代入演算子の定義は次のようにしていた
A& operator = (const A& obj)
しかし,引数のconstなしや,参照ではなく,実体を返しても定義としては間違っていない.しかし,ここはこのように定義するべきである.
まず,constが必要なのは,C++の場合、一時的なインスタンスを関数に参照渡しするとき、const を指定しないとコンパイルエラーになるからである.仮にconstなしでコンパイルしてみると,
4.cpp:29:5: error: no viable overloaded '='
a1 = getA2();
~~ ^ ~~~~~~~
4.cpp:11:5: note: candidate function not viable: expects an l-value for 1st argument
A& operator = (A& obj) {
^
というエラーがでる.これは,main
関数で
int main() {
A a1;
a1 = getA2(); --->一時オブジェクトが生成される
}
としているためであり,getA2()
の呼び出しで返されるオブジェクトは,どの変数にも代入されない一時オブジェクトに当たるためである.仮にa1 = a1
とすればコンパイルは通る(意味のないコードだけど).これをconst
なしで扱うには,右辺値参照やMoveセマンティクスを学ぶ必要があるが,ここでは割愛.
次に,返り値はAの参照を返しているが,ここは実体でもコンパイルは通る.しかし,これは参照にするべきである.
例えば,
a = (b = c)
を考える.このコードは
a = b.operator=(c)
と同等であり,b
の代入演算子が実行された後a
に代入されなければならないのはb
自身であるべきだからである.
もし,参照じゃなく実体を返す,ということになると,return *this
のときにコピーが作られ,それが返されることになり,b
とは独立したオブジェクトが返ることになる.これはプログラマの意図としてはおかしい.
実際,
A operator = (const A& obj) {
return *this;
}
としてみると,
A a1;
a1 = getA2()
の実行結果は,
Constructor A
getA2
Constructor A
insert
copy Constructor A
となり,最後にコピーコンストラクタが実行されていることがわかる.
最後に,ここで用いた全ソースコードを載せておく
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "Constructor A" << endl;}
A(const A &obj) {
cout << "copy Constructor A" << endl;
}
A operator = (const A& obj) {
cout << "insert" << endl;
return *this;
}
};
A getA(A obj) {
return obj;
}
A getA2() {
cout << "getA2" << endl;
A a;
return a;
}
int main() {
A a1;
a1 = getA2();
}