C++ のコピーコンストラクターと代入演算子
C++ でクラスを作ったり、メンバーを追加した場合、必ずやらなければならないことがあります。 それは 「コピーコンストラクター と 代入演算子 が必要か適切に判断する」ということです。 また、必要だったとして、これらを書く場合にも様々な注意点やテクニックがあります。 今回はコピーコンストラクターと代入演算子に関する話題についてまとめてみたいと思います。
なお、今回はEffective C++の影響をかなり受けています。
Effective C++と同じような内容を書くと問題あるかと思い、 それらに関してはサワリと本の項の参照先だけ書いています。
(項は第 2 版のものです)
コピーコンストラクター、代入演算子 とは
まず、この 2 つについて、概要を説明します。コピーコンストラクターは 同じクラスのオブジェクトを引数にとるコンストラクター を指します。
Foo a; Foo b(a); // コピーコンスタクター代入演算子は 同じクラスのオブジェクトを代入する時 に使われます。
Foo a, c; c = a; // 代入演算子この 2 つは同じようなことをやるので、基本的にセットで考えます。 最初(初期化時)にコピーするのがコピーコンストラクターで、途中でコピーするのが代入演算子です。
ちょっと意外かもしれませんが、初期化時には代入に見えてもコピーコンストラクターが呼ばれます。
Foo a; Foo d = a; // コピーコンスタクターコピーコンスタクター、代入演算子の 2 つには大きなポイントがあります。
- 定義しなくても使える(デフォルトのものが呼び出される)
- デフォルトのものだと問題が生じることがある
定義方針
前章のポイントを踏まえてると、コピーコンストラクターと代入演算子を定義する方針は次のようになります。なぜこういう方針になるのか、定義時の注意事項も含め、順に解説していきます。
デフォルトの仕様
コピーコンストラクター、代入演算子は記述しなくても、コンパイラーが自動で作成してくれるため、 コピー、代入を行うことができます。このことを知らないのか、知っててもちゃんと判断できないのか、 やたら書く人がいます。
しかし、これはよくありません。 時間の無駄ですし、メンバーの追加や継承を行った際に忘れてしまう危険性が出てきます。 不必要なコピーコンストラクター、代入演算子は定義しないようにしなければなりません。
不必要かどうか判断するためには、デフォルトで作成されるコピーコンストラクター、代入演算子 がどういうものか知っておく必要があります。
デフォルトで作成されるものでも、memcpy のような単純なコピーではありません。 「各メンバーに対して、コピーコンストラクターあるいは代入演算子を順に呼び出す」 という仕様になっています。
Effective C++
(45 項) C++ がどんな関数を黙って書き、呼び出しているか知っておこう
noncopy_sample.cpp :
class Person { private: std::string m_name; int m_age; // : };int など基本型のメンバーに関してはデフォルトのもので十分なのは、すぐわかると思います。
string のクラス自体は内部に動的に割り当てるメモリーを持っています。 しかし、string がコピーコンストラクター、代入演算子を持っていて、 それを呼び出すため、問題なく処理されます。
よって、このクラスではコピーコンストラクター、代入演算子の定義は不要です。
コピーコンストラクター、代入演算子の定義が必要な場合
定義が必要なのはメモリーを動的に割り当てるクラスです。 これはポインターのコピーでは同じものを指すために生じる問題です。Effective C++
(11 項) メモリを動的に割り当てるクラスでは、コピーコンストラクタと代入演算子を宣言しよう
コピーコンストラクター、代入演算子の定義方法
コピーコンストラクターと代入演算子の定義の書式は次のようになります。C(const C &); C &operator=(const C &);具体的なサンプルをあげます。
copy_sample.cpp : (抜粋)
class Person { char *m_name; ///< 名前 unsigned int m_age; ///< 年齢 public: /// コンストラクター Person(const char *name = 0, unsigned int age = 0) :m_name(0), m_age(age) { SetName(name); } /// デストラクター. virtual ~Person() { SetName(0); } /// コピーコンストラクター Person(const Person &other) :m_age(other.m_age) { SetName(other.m_name); } Person &operator=(const Person &other) { // 自身の代入チェック if (this != &other) { SetName(other.m_name); m_age = other.m_age; } return *this; } // : };コピーコンストラクターと代入演算子を書く上でいくつか注意点があります。
代入演算子の戻り値は自身の参照
代入演算子では
*this
を返してます。
Person &operator=(const Person &other) { // : return *this; }ここは void でも実装は可能です。 ただ、そうすると基本型のように
a = b = 1;
といった連続した代入ができなくなリます。
そのため、自身の参照(*this)を返すのが流儀です。
Effective C++
(15 項) operator = を書くときは、*this へのリファレンスを返そう
代入演算子では自身の代入チェック
サンプルで this
かどうかをチェックしている部分です。
Person &operator=(const Person &other) { // 自身の代入チェック if (this != &other) { // : } return *this; }コピーコンストラクターとは違い、代入では自身が渡される可能性があります。 代入演算子を定義した場合には必ず自身のチェックを入れる習慣をつけた方がよいです。
Effective C++
(17 項) operator = では、自分自身へ代入するケースをチェックしよう
継承クラスでのコピー、代入
コピーコンストラクター、代入演算子で間違えやすいのが、 継承したクラスの場合です。copy_sample.cpp : (抜粋)
class Student : public Person { char *m_school; ///< 学校名 public: /// コンストラクター Student(const char *name = 0, unsigned int age = 0, const char *school = 0) :Person(name, age) { SetSchool(school); } /// デストラクター. virtual ~Student() { SetSchool(0); } /// コピーコンストラクター Student(const Student &other) :Person(other) { SetSchool(other.m_school); } Student &operator=(const Student &other) { // 自身の代入チェック if (this != &other) { // 基底クラスメンバーの代入 Person::operator=(other); SetSchool(other.m_school); } return *this; } // : };コピーコンストラクターでは初期化子で基底クラスのコピーコンストラクターを指定する必要があります。 これを忘れると基底クラスのメンバーはデフォルトコンストラクターで初期化されてしまいます。
Student(const Student &other) :Person(other) // 基底クラスのコピーコンスタクター {代入演算子でも明示的に基底クラスの代入演算子を呼び出す必要があります。
Student &operator=(const Student &other) { // : // 基底クラスメンバーの代入 Person::operator=(other); SetSchool(other.m_school);
Effective C++
(16 項) operator = では、すべてのデータメンバに代入しよう
代入演算子によるコピーコンストラクター実装の是非
DRY(Don't Repeat Yourself)原則とあるように、同じことを繰り返さないのがいいプログラミングです。コピーコンストラクター、代入演算子は同じような処理を行います。 コピーコンストラクターを代入演算子を使って実装することを考えてみます。
Person(const Person &other) { *this = other; }個人的にはこれも 「悪くはない」と思います。
ただ、これだとデフォルトでの初期化の後、代入となります。 そのため、メンバーの初期化演算子で設定した方が、若干速くなるはずです。 コピーコンストラクターも初期化子を使って別途、実装した方がよりよいです。
また、メンバーが const やリファレンスとなっている場合、 それらはそもそもコンストラクターでしか設定できません。 そういった場合は代入の方は後述の「禁止」にすることが多いと思います。
Effective C++
(12 項) コンストラクタでは、代入よりも初期化を使おう
コピー、代入の禁止
通常のメンバー関数は、定義しなければ使えません。 しかし、コピーコンストラクター、代入演算子は書かなくても自動で作成されます。 そのため、使えないようするには明示的に「禁止」する必要があります。コピー、代入の禁止が必要なケース
前章のコピーコンストラクター、代入演算子の定義が必要な場合だったとしても、 アプリケーションのクラスではコピー、代入の処理自体が必要ない場合も結構あります。 こういった場合はわざわざ実装せず、コピー、代入の禁止にします。代入できないメンバーを持つ場合も代入を禁止します。代入できないメンバーは次の 2 つです。
- const
- リファレンス
コピー、代入してはいけないメンバーを持つ場合もあります。
例えば、fstream やファイルポインター(FILE *)のようなファイルを扱う変数です。 これらはコピーして複数で使うとおかしなことになります。
なお fstream のように次節以降の方法でコピー、代入が禁止され、使えないようになっていることもあります。
禁止方法 - 基本
コンスタクターが自動で作ってしまう関数を禁止するには 関数を private にします。Effective C++
(27 項) 暗黙のうちに生成される不要なメンバ関数は、明示的に使用を禁止しよう
class Person { private: // コピー禁止 Person(const Person &); void operator=(const Person &);ここで代入演算子の戻り値を void にしていますが、 「戻り値だけ違う関数は定義できない」ため、なんでもかまいません。
禁止方法 - マクロ
コピー、代入を禁止するクラスを作ることは結構あります。 毎回書くのが面倒だったり、禁止する目的を明確にしたりするため、 次のようなマクロを使うこともあります。/// コピー禁止マクロ /// @param C クラス名 #define NON_COPYABLE(C) C(const C &); \ void operator=(const C &)これを使う場合、 このマクロを private 領域に記述します。
class Person { private: // コピー禁止(マクロ版) NON_COPYABLE(Person);ただ、C++ ではマクロはあまり推奨されていませんし、 マクロ自体をコーディング規約で禁止されていることもあります。
Effective C++
(27 項) #define ではなく、const と inline を使おう
禁止方法 - 継承 (boost::noncopyable)
直接書かなくても、private な継承を利用して関数を private にすることも可能です。 そのためのクラスを用意しておけば、マクロのように記述を簡略化、明確化することができます。Boost では、すでにそのクラスが用意されており、すぐに使うことができます。
#include <boost/noncopyable.hpp> class Person : boost::noncopyable {ただし、boost::noncopyable は使えない場合があります。
それは 他のクラスを継承しなければならない場合です。 その場合、多重継承で noncopyable を継承することになりますが、 boost::noncopyable は多重継承に対応していません。
多重継承に対応しようとするともう少し複雑になります。 ここまでやるなら、もう直接書くかマクロでもいいかなとは思います。
禁止方法 - C++11
C++11 では、関数の使用を禁止する delete 指定 の機能が追加されました。 これを使って、コピーコンストラクター、代入演算子を禁止すると次のようになります。class Person { public: // コピー禁止 (C++11) Person(const Person &) = delete; Person &operator=(const Person &) = delete;マクロにすると以下のような感じです。
/// コピー禁止マクロ (C++11) /// @param C クラス名 #define NON_COPYABLE(C) C& operator=(const C&) = delete; \ C(const C&) = delete;従来の方法と違い、 private 領域に書かないといけないという縛りがなくなります。
また、コンパイル時のエラーメッセージも「private なメソッドを使っている」から「削除されたメソッドを使っている」という旨のメッセージに変わり、原因が分かりやすくなっています。
なお、実際に C++11 を使う場合は移動コンストラクター、移動代入演算子もも考慮した方がいいのです。 そちらについては以下の記事をご覧ください。
サンプルコード
コピー禁止のサンプルコードです。- 関連記事
Facebook コメント
コメント
No title
> if (this != &other) {
じゃない
Re: No title
> > if (this != &other) {
> じゃない
指摘ありがとうございます。
確かに間違えてましたので、修正しました。
確認
参考になります
上のコメントで指摘されている修正内容(!=)がリンク先の.cppファイル側(copy_sample.cpp)に反映されて無い様に見えます
(html側は修正されている様です)
No title
修正事項
私のようにバグで時間を費やしてしまう方がいるかもしれないので,勝手ながら下記に修正を列挙させていただきます。
(本ブログは数年間更新されていないようなので,本記事に反映される可能性は低いと思いますが。)
・53行目:Personクラスの代入演算子における「自身の代入チェック」において
if (this != &other)
が正しいです。(132行目のStudentクラスの代入演算子では上記のようになっているので問題なし)
・45行目のPersonクラスのコピーコンストラクタの初期化子リストを
:m_name(0),m_age(other.m_age)
として,m_nameに関して0で初期化する。
・108行目のStudentクラスのコンストラクタの初期化子リストを
:Person(name, age),m_school(0)
として,m_schoolに関して0で初期化する。
・124行目のStudentクラスのコピーコンストラクタの初期化子リストを
:Person(other),m_school(0)
として,m_schoolに関して0で初期化する。
上記3つの初期化がないと,各charポインタは不明なアドレスを指したままの状態になってしまいます。
その場合,各コンストラクタが呼び出す関数SetName()とSetShool()内部のif (m_name)もしくはif (m_school)の判定がtrueになります。
その結果,new演算子で確保していない領域をdeleteすることになってしまい,Segmentation fault.となります。
ちなみに,なぜかPersonクラスのコンストラクタの方ではm_name(0)も書かれており,こちらは問題なく初期化しています。
また,初期化はm_name(nullptr),m_name{nullptr}などでもいいと思いますし,
各クラスのメンバ変数宣言(23,103行目)の時点でデフォルトメンバ初期化子を利用して初めから0やnullptrにしておいても良いと思います。
・71行目,150行目の各クラスのSetterのdelete演算子を
delete[] m_name
delete[] m_school
として,配列用のものに変える。
m_name,m_schoolは各Setterにおいてnew演算子によりleng+1サイズ分確保される配列を指します。
配列に対してはdeleteだと未定義動作です。
ちなみに183行目
cout << michael << endl; // {"Michael"(21)}-"Foo University"
とありますが,意図しているものとしては
cout << tom << endl; // {"Tom"(21)}-"Foo University"
と思われます。