こんにちは。暑い日が続いてます。ちょっと夏バテ気味の田原です。
さて、C++のクラスにはスペシャルなメンバ関数が6個あります。このように書くとなんだか特別でC++らしく非常に難しいメンバ関数がありそうですが、実は全くそんなことないです。日常的にありふれています。あなたが書いた「普通」のclassにもきっとはいってます。今回は、そんなメンバ関数について解説します。
1.特殊メンバ関数
規格書では “Special member functions”とかかれています。日本語では「特殊メンバ関数」や「特別メンバ関数」と呼ぶ人が多いようです。
何が特別かと言いますと、コンパイラが自動的に定義することが有る点です。あなたがコードを書かなくても自動的にコンパイラがコードを用意してくれる便利なものです。
次の6個あります。
- デフォルト・コンストラクタ
- デストラクタ
- コピー・コンストラクタ
- コピー代入演算子
- ムーブ・コンストラクタ
- ムーブ代入演算子
1.~4.はC++の初期からありました。5.,6.はC++11で(劇的に)追加されました。
今回は1.~4.について主に解説します。5.と6.は軽く触れて次回から本格的に解説します。
2.自動生成されるデフォルト・コンストラクタとデストラクタ
コンストラクタを1つも定義してなく、かつ、自動生成可能な時(*1)
は、デフォルト・コンストラクタが自動生成されます。
同様にデストラクタを定義していない時は自動的に生成されます。
例えば、次のサンプルでBarクラスはコンストラクタもデストラクタも定義していません。しかし、基底クラスFooとメンバ変数mFooについてコンストラクタとデストラクタが呼ばれます。
#include <iostream> struct Foo { Foo() { std::cout << "Foo:Foo()\n"; } ~Foo() { std::cout << "Foo:~Foo()\n"; } }; class Bar : public Foo { Foo mFoo; }; int main() { Bar bar; std::cout << "end of main()\n"; }
これらの基底クラスとメンバ変数のコンストラクタとデストラクタを呼び出しているものは、コンパイラが自動生成したBarのコンストラクタとデストラクタです。
当然、自動生成ですので単純なものしか生成されません。この例のケースでは次のようなイメージになります。
public: Bar() : Foo(), mFoo() { } ~Bar() { }
自動生成可能でない時
例えば、メンバ変数に参照が含まれると、参照先の指定が必要なのでデフォルト・コンストラクタの自動生成はできません。
mIntReferenceはコンストラクト時に初期化する必要がありますが、参照先を自動的に決めることができないため、デフォルト・コンストラクタを自動生成できないわけです。class Foo { int& mIntReference; };
3.代入演算子
これまでも時々使っていたのですが、+や-等の演算子をメンバ関数として定義することができます。
通常のメンバ関数は関数名、戻り値、引数の数と型は完全に自由ですが、演算子をメンバ関数として定義する場合は、関数名と引数の数は決められていて自由に選択できません。
3-1.代入演算子の悪用例
さて、代入演算子=があります。関数名は”operator=”です。引数の数は1つです。他は全く自由ですので、戻り値や引数の型、実際の機能を自由に定義できます。しかし、字面から受け取る印象と全く異なるような定義をしないよう強くお勧めします。
例えば、次のクラスは逆方向に代入する代入演算子を定義しました。(「よいこはまねをしないで下さい。」ですね。)
#include <iostream> class Foo { int mData; public: Foo(int iData) : mData(iData) { } void operator=(Foo& oRhs) { oRhs.mData = mData; } int get() { return mData; } }; int main() { Foo a(123); Foo b(456); a=b; std::cout << "a=" << a.get() << "\n"; std::cout << "b=" << b.get() << "\n"; }
a=b;により、aの値がbへ代入されています。普通は逆です。これは酷いですね。
a=b;を見てaがbに代入されると予想できる人はいないでしょう。なかなか取れないバグの元になります。
このような「悪用」は絶対やめましょう。他のプロジェクト・メンバーへ酷い迷惑をかけますので。
3-2.コピー代入演算子の自動生成
さて、先程の酷いサンプルの8行目にあるoperator=()をWandbox上でコメントアウトしてみて下さい。これにより同じクラスを引数とする代入演算子が存在しなくなります。存在しないものを呼べませんから、コンパイル・エラーになりそうなものですが、実際にはエラーになりません。
デフォルト・コンストラクタと同じく、自動的にコンパイラが生成するからです。
次のようなコードが自動的に生成されています。
Foo& operator=(Foo const& iRhs) { mData = iRhs.mData; return *this; }
(今回は基底クラスがありませんが有る時は)基底クラス、および、メンバ変数を単純に「全て」代入するだけの演算子が自動的に定義されます。これを「コピー代入演算子」と呼びます。
普通の代入は右辺の値を左辺の変数へコピーしますから、以前は「代入演算子」とシンプルに呼ばれていました。C++11でムーブ代入演算子を定義できるようになったので、これと区別するためコピー代入演算子と明記することが多いです。
3-3.コピー代入演算子ではまりやすいこと
各メンバ変数の「意味」はコンパイラには伝わりませんから、コンパイラは意味を考えることなくコピーします。
それがポインタだった場合も、単純にポインタに入っているアドレスをコピーします。ポインタが指すオブジェクトをコピーしたりしません。
これにまつわる有名な不具合があります。
Fooオブジェクトのポインタを管理するため、RAIIパターンでBarクラスが破棄される時、一緒にFooオブジェクトも破棄するようにしました。
class Foo { int mData; public: Foo(int iData) : mData(iData) { } int get() { return mData; } }; class Bar { Foo* mFoo; public: Bar() : mFoo(nullptr) { } Bar(Foo* iFoo) : mFoo(iFoo) { std::cout << "new Foo =" << mFoo << std::endl; } ~Bar() { if (mFoo != nullptr) { std::cout << "delete mFoo = " << mFoo << std::endl; delete mFoo; std::cout << "deleted." << std::endl; } } int get() { return (mFoo)?mFoo->get():-1; } };
例えば、次のように使えます。
int main() { Bar bar0(new Foo(123)); // bar0が生きている限り、bar0内のmFooも有効。 std::cout << bar0.get() << "\n"; // main()関数が終了しbar0が破棄(デストラクト)される時、自動的にmFooも解放される。 // mFooを解放するためのコードを書かなくても良いので、書き忘れることがなく、 // 例外でぴょんと通り越されても、きちんと破棄される。 }
スマートですね。第29回で解説したように良く使われるテクニックです。
しかし、問題があります。代入演算子を定義していないのでコピーできないかと思うと、そんなことないです。上述したような単純なコピー代入演算子が自動生成されてしまいます。
こんなイメージです。
Bar& operator=(Bar const& iRhs) { mFoo = iRhs.mFoo; return *this; }
単純にFooへのポインタであるmFooをコピーするだけです。
このケースの場合、bar0でnewされたFoo型へのポインタmFooを単純にコピーし、mFooが指す先のオブジェクトをコピーするわけではありません。
int main() { Bar bar0(new Foo(123)); Bar bar1; bar1 = bar0; }
上記のような場合、main()関数終了時にスコープから外れるためbar0とbar1が解放されるのですが、その時それぞれのデストラクタが呼ばれます。その際、両者のmFooに設定されているアドレスはどちらともbar0のコンストラクト時にnewが返却したアドレスです。
つまり、bar0のデストラクタでdeleteされ、bar1のデストラクタで先程と同じアドレスが再度deleteされます。その結果、ヒープ管理が可笑しくなりプログラムが異常終了します。
Wandboxで確認する(多量のエラーメッセージが表示されますので、出力ウィンドウを上の方までスクロールしてみて下さい。)
3-4.不憫なstd::auto_ptr<>のお話
先程のようなポインタを自動的に解放してくれるクラスが標準ライブラリにあると便利ですね。
実はあります。スマート・ポインタと呼ばれています。C++11で定義された新しいものもあるのですが、今回は、それ以前からある不憫なstd::auto_ptr<>
のお話をします。
先程のBarはFooクラスしか管理できませんが、Fooクラスにあたる部分をテンプレートという仕組みを使ってほとんどの型に対応したクラスがstd::auto_ptr<>
です。
std::auto_ptr<>
は3-3.の問題を回避するため、上記のBarに次のような代入演算子を追加定義した感じになっています。(実際には遥かにもっと便利に定義されてます。ここでは解説のために超簡単化してます。)
Bar& operator=(Bar& iRhs) { if (this != &iRhs) { delete mFoo; mFoo = iRhs.mFoo; iRhs.mFoo = nullptr; } return *this; }
2017/09/04 : 修正しました。ムーブ代入演算子のサンプルに自分チェックと元のポインタのdeleteを追加しています。
元のポインタをdeleteしないとリークします。自分が渡された時に処理すると破綻しますので自分でない時のみ処理します。
3-3.のサンプルにWandbox上でBarクラスの最後付近に上記コードを挿入してみて下さい。これにより、落ちなくなることを確認できます。
bar0のmFooはnullptrになっているのでbar0解放時代入前のmFooは解放されません。従って、bar1でのみ解放され同じアドレスが2回解放されることがないので落ちません。ありがたいことです。
しかし、気がついた方もいらっしゃると思いますが、上記のコピー代入演算子は「3-1.代入演算子の悪用例」に該当するようなコピー代入演算子です。a=b;と書いたときのbを修正しています。確認のため、bar1 = bar0;
の前後でbar0を表示してみましょう。
int main() { Bar bar0(new Foo(123)); Bar bar1; std::cout << "bar0 : " << bar0.get() << "\n"; bar1 = bar0; std::cout << "bar0 : " << bar0.get() << "\n"; }
一見単なる代入なのですが、右辺の値が変わってます。なかなか嫌なことです。
これが原因で、C++11でstd::auto_ptr<>
は非推奨となりました。代わりにstd::unique_ptr<>
が用意されています。
4.コピー・コンストラクタ
さて、コピーできないって不便ですね。そんな時は自分で作れば良いのです。
Bar& operator=(Bar const& iRhs) { if (this != &iRhs) { delete mFoo; mFoo = new Foo(*iRhs.mFoo); } return *this; }
2017/09/10 : 修正しました。自分チェックと元のポインタのdeleteを追加しています。
元のポインタをdeleteしないとリークします。自分が渡された時に処理すると破綻しますので自分でない時のみ処理します。
さっきの代入演算子の代わりにこれを定義するとmFooの指すオブジェクトもコピーします。
bar1のmFooはこのコピーしたものをポイントするため、bar1の開放時はコピーしたFooが解放され、bar0の開放時は元のものが解放されます。スムーズですね。
Wandboxで確認する
ところで、new Foo(*iRhs.mFoo);
で呼び出しているものは、当たり前ですが*iRhs.mFoo
を受け取るコンストラクタです。
iRhs.mFoo
はFoo型へのポインタです。そして、*
は間接演算子(ポインタの指すオブジェクトを返却)ですから、*iRhs.mFoo
はオブジェクトFooそのものを返却します。(厳密にはオブジェクトFooへの参照です。)
つまり、Foo(Foo& iFoo);
のようなコンストラクタが呼ばれます。
しかし、これも定義していませんが、エラーになりません。つまり、これも自動生成されます。
実は、次のような「コピー・コンストラクタ」が自動生成されます。
Foo(Foo const& iFoo) : mData(iFoo.mData) { }
最初に出てきた代入演算子とちょっと似てますが、こちらはコンストラクタです。何が違うのかというと、代入演算子は既に確保されている変数へ代入します。コピー・コンストラクタは変数領域を獲得し、受け取ったオブジェクトの内容を獲得した領域へコピーします。
5.まとめ
今回は、次のような特殊メンバ関数がでてきました。
名前 | 自動生成される関数 |
---|---|
デフォルト・コンストラクタ | Foo() : 基底クラスとメンバのデフォルト初期化子リスト { } |
デストラクタ | ~Foo() { } |
コピー代入演算子 | Foo& operator=(Foo const& iRhs) { 基底クラスとメンバのコピー; } |
コピー・コンストラクタ | Foo() : 基底クラスとメンバをコピーする初期化子リスト { } |
C++11より前の標準規格は以上でした。C++11で、これに次の2つが追加されました。
- ムーブ代入演算子
- ムーブ・コンストラクタ
先に示したstd::auto_ptr<>
は、ポインタをコピーして、コピー元をnullptrにしています。これはコピー元が管理するオブジェクトをコピー先へ「移動」したという意味で捉えると分かりやすいです。これがつまり要するに「ムーブ」です。
実はこのムーブとそれを実現するための右辺値参照がC++規格の近年の最大の変化と思います。
ポインタは結構難しいと言われています。参照はポインタより理解するのに苦労します。変数じゃないけど初期化できるから定数ではないし、私は理解するのに結構苦労した口です。
そして、C++11では更に不思議な参照が定義されました。右辺値参照です。これを使うことでムーブを実にスマートに記述できるようになりました。
C++11より前は、バグを防ぐために必要もないのにコピーして負荷をかけるか、間違ってムーブしてバグを生むリスクを犯すかの2択だったのが、右辺値参照により間違ってムーブするリスクを回避しつつ無駄なコピーを避けることができる画期的な手法と思います。しかし、その分、難しいです。
ところで、右辺値参照って左辺値です。何を言っているのか分からんでしょう? これが右辺値参照が難しい所以です。
来週から上記のムーブ関係の特殊メンバ関数と右辺値参照について解説します。お楽しみに。