コピー禁止を徹底させるNoncopyableクラス
クラスのオブジェクトは至る所でコピーが発生しています。これには2つの方法が使い分けられています。以下を御覧下さい:
MyClass obj1;
MyClass obj2(obj1); // コピーコンストラクタによるコピー
MyClass obj3 = obj1; // これもコピーコンストラクタ!
func(obj1); // 関数の引数はコピーコンストラクタ
MyClass obj4;
obj4 = obj1; // 代入演算子によるコピー
コピーには「コピーコンストラクタによるコピー」と「代入演算子によるコピー」があります。コピーコンストラクタとは次のような定形コンストラクタです:
class MyClass {
int val;
public:
MyClass(const MyClass &src) {
val = src.val;
}
};
コピーコンストラクタを自前で書いた場合、上のように自分のメンバーのコピーを責任をもって行う必要があります。「普段コピーコンストラクタなんて書いて無いけど…」という方もいらっしゃると思います。コピーコンストラクタが明示的に書かれていない場合、コンパイラは自動的にコピーコンストラクタを作ってくれています。それは、上のような「const MyClass &src」という引数を持ったコンストラクタで、コピーはメンバ変数全部の「代入演算子」を使います。
代入演算子(=)によるコピーは明示的に書くと次のように定義出来ます:
class MyClass {
int val;
public:
void operator =(const MyClass &src) {
val = src.val;
}
};
コピーコンストラクタと引数は全く同じで、内部で引数のオブジェクトのメンバをやはり代入演算子でコピーします。これも明示的に定義しなくてもコンパイラが上のように作ってくれます。
@ オブジェクトコピーの禁止
オブジェクトをコピーされると困る場合があります。例えば、クラスの内部でオブジェクトをnewして、デストラクタでdeleteしている場合です。次を御覧下さい:
class MyClass {
int *val;
public:
MyClass() : val(new int(0)) {
}
~MyClass() {
delete val;
}
};
コンストラクタで*valに有効なポインタを代入して、デストラクタでそれをdeleteしています。このクラスのオブジェクトを例えば代入演算子でコピーするとどうなるでしょうか?
MyClass obj1, obj2;
obj1 = obj2;
代入演算子はデフォルトでは内部でメンバ変数を単純に代入するのでした。と言うことはobj1のポインタ変数valがobj2のそれで上書きされてしまいます。この段階でobj1.valはもう迷子で一生消されることは無くなってしまいます。それだけでなく、obj1のデストラクタは代入されたobj2.valのポインタを消してしまいます(obj2.valがダングリング状態)。そして、obj2が消される直前にそのデストラクタが呼ばれた段階で、不定なポインタを消そうとするためメモリ保護違反で落ちます。たった1回の代入で、ここまで悲劇的なことが連鎖するんです!
「MyClassのオブジェクトをコピーしないように」とお願いしても、冒頭に挙げたようにどこか知らない所でコピーは起きます。人の目で制御するのは実質不可能でしょう。そこで、機能的にコピーできないようにしてしまいます。
例えば、上のように代入演算子によるコピーを禁止するには、代入演算子をprivate宣言でオーバーライドします:
class MyClass {
int *val;
public:
MyClass() : val(new int(0)) {
}
~MyClass() {
delete val;
}
private:
void operator =(const MyClass& src) {}
};
この1行を加えるだけで、先の代入演算子によるコピーが出来なくなります。=演算子(実質関数)がprivateなのでクラスの外の人が呼べないためです。
ただ、次のような代入はできてしまいます:
MyClass obj1;
MyClass obj2 = obj1;
これはコピーコンストラクタによる代入のため、MyClassの代入演算子が使われないためです。つまり、先程の悲惨なバグが再び発生します。これを防ぐにはコピーコンストラクタもprivateでオーバーライドします:
class MyClass {
int *val;
public:
MyClass() : val(new int(0)) {
}
~MyClass() {
delete val;
}
private:
void operator =(const MyClass& src) {}
MyClass(const MyClass& src) {}
};
これでMyClassは一切の代入行為、つまりコピーができなくなります。通常、オブジェクトのコピーを禁止したい場合はコンピーコンストラクタと代入演算子の両方を上のようにprivate宣言します。
一切のコピー行為を禁止することで成り立っているのが「シングルトンパターン」です。このパターンはさらにデフォルトコンストラクタまでもprivateにする徹底ぶりです。オブジェクトの生成すら禁止しているわけです。
A Noncopyableクラスでコピー禁止属性を付ける
さて、上のようにすればコピー禁止になるのですが、毎回作るクラスにコピーコンストラクタと代入演算子をオーバーライドするのは面倒です。そこで、これらのオーバーライドだけを行っている次のようなクラスを作ります:
class Noncopyable {
public:
Noncopyable() {} // コンストラクタはいります
private:
void operator =(const Noncopyable& src) {}
Noncopyable(const Noncopyable& src) {}
}
そして、コピーを禁止したいクラスはこのクラスを親に持つようにします:
#include "Noncopyable.h"
class MyClass : public Noncopyable {
int *val;
public:
MyClass() : val(new int(0)) {
}
~MyClass() {
delete val;
}
};
こうするだけでコピー禁止属性が付きます。親にするだけなのですから異常に簡単ですよね。
Noncopyableクラスにはコンストラクタが必要です。これが無いと、オブジェクトの生成すら禁止されてしまいます。意図的にそういう超禁止属性を付けるのもありです。
コピー禁止属性を付ける事で、上のような内部でnew、デストラクタでdeleteするタイプのクラスはかなり安全に扱う事ができるようになります。ただ、逆にコピー自体が出来なくなってしまうので、cloneメソッドのような明示的コピーを促すメソッドを追加する必要が出てきます。その場合はNoncopyable属性を付けずにコピーコンストラクタと代入演算子を適切にオーバーライドした方が扱いは楽になります。
(2011.11.20追記)
B より堅牢なNoncopyable
上の実装について、りお様よりより堅牢な実装方法を教わりました。
上のNoncopyableクラスは機能はします。ただ、Noncopyableオブジェクトを作れます。コンストラクタがpublicとして公開されているためです。これをprotectedにするとコンストラクタが非公開になるのでNoncopyableオブジェクトすら作れなくなります。
次に、コピーコンストラクタと代入演算子ですが、これは宣言だけにします。「え!」と思うかもしれませんが、クラスで宣言されたメソッドは、それが実装で呼ばれなければ宣言そのものが無かった事にされます(最適化の一貫)。逆に実装で呼ばれた時に「そのメソッドの実体はないぜ!」とコンパイラに怒られます。こんな感じです:
コンパイルエラー 1>------ ビルド開始: プロジェクト: Noncopyable, 構成: Debug Win32 ------
1>コンパイルしています...
1>Noncopyable.cpp
1>c:\***\noncopyable.cpp(20) : error C2248: 'Noncopyable::operator =' : private メンバ (クラス 'Noncopyable' で宣言されている) にアクセスできません。
より堅牢なNoncopyableクラスは次のとおりです:
class Noncopyable {
protected:
Noncopyable() {}
~Noncopyable() {}
private:
void operator =(const Noncopyable& src);
Noncopyable(const Noncopyable& src);
};
さらに、コピー禁止属性を付けたいクラスに対してはNoncopyableクラスは「private」で継承します:
#include "Noncopyable.h"
class MyClass : private Noncopyable {
int *val;
public:
MyClass() : val(new int(0)) {
}
~MyClass() {
delete val;
}
};
こうするとコンパイラの最適化が促進されるとの事です。奥深いもんです(-_-;。