この記事について
前回に引き続きScott Meyers著Effective C++の項11〜15を解説します。
項11 自己代入に備えよう(Handle assignment to self in operator=)
自己代入とは以下のようなコードを指します。
Object a;
a = a;
と、思うかもしれませんがそんなこともありません。
たとえば、配列を並べ替える処理を書いている時にfor文と配列を使ってこのような記述が見られます。
a[i] = b[j];
また、ポインタをつかって操作をしている時にポインタが何を指しているか忘れたり
*pa = *pb;
あるあるですね。
この問題に対処する方法は2つあります。
ひとつは同一性テスト。
オブジェクトのアドレスを比較して同じだったらコピーしないという方法です。
Object& Object::operator=(const Object& rhs)
{
if(this==&rhs) return *this;
/* 後は普通のコピー */
return *this;
}
もうひとつはコピー・アンド・スワップというテクニックを使う方法です。
引数として与えられたオブジェクトを一旦コピーしてから、コピーされたオブジェクトと入れ替えるという方法です。std::swapを使えば簡潔に書くことができます。
Object& Object::operator=(const Object& rhs)
{
Object temp(rhs);
/* 後はtempと*this を入れ換える */
return *this;
}
項12 オブジェクトの全ての部分をコピーしよう(Copy all parts of an object)
これを守らないと極度に直感に反する動作をするようになります。
絶対に守ってください。
これが起きやすいのは継承した時です。
継承時に新しいメンバを加えたあとで代入演算子、コピーコンストラクタの修正を忘れると新しく追加したメンバはコピーされないです。コレに対処するには継承先でもしっかりコピーを定義しましょう。
class Base
{
double a;
public:
void init(const Base& rhs)
{
std::swap(this->a, rhs.a)
return;
}
Base& operator=(const Base& rhs)
{
init(rhs);
retrun *this;
}
};
class Child:public Base
{
double b;
public:
void init(const Child& rhs)
{
Base::init(rhs);
Child temp(rhs);
std::swap(this->b, temp.b);
return;
}
Child& operator=(const Child& rhs)
{
init(rhs);
}
};
項13 リソース管理にオブジェクトを使おう(Use objects to manage resources)
メモリリークはC++の永遠の宿敵です。
この敵を打ち倒すために生まれたのがリソースを管理するクラスであるスマートポインタです。
スマートポインタを使う利点には2つあります。
std::tr1::shared_ptr<Object> a(new Object);
ひとつはコンストラクタをつかって初期化(RAII)が実現できること。
もうひとつはスマートポインタを使うとクラスがスコープが外れると自動的にメモリを解放することです。
注意しなければならないのはスマートポインタは配列には使えないことです。
デストラクタでdelete [] ではなくdeleteを呼び出しているため配列にスマートポインタを使おうとするとリークします。
std::tr1::shared_ptr<Object> b(new Object[10]); // delete []されない。
これに対処方法は配列の代わりにvectorなどのコンテナを使いましょう。
std::vector<Object> b(10);
他にもスマートポインターのデリータをカスタムすることでdelete []をデストラクタで呼び出すようにする方法がありますが、コードが必要以上に複雑になるのでおすすめしません。
項14 リソース管理クラスのコピー動作を注意しよう(Think carefully about copying behavior in resource-managing classes)
リソースを管理するクラスのコピーの挙動にはいくつかあります。
注意しないと思わぬバグの原因になります。
主なコピーの挙動は以下のような感じ。
- コピー禁止
多くのクラスに見られます。方法については項6を参照してください。 - 管理しているリソースの参照カウント
コピーするととともに参照カウントをインクリメントします。
どのクラスからも参照されなくなった時点でリソースを解放します。
shared_ptrで見られます。 - 管理しているリソースをコピーする。
いわゆる深いコピーを行います。
std::stringで見られます。 - 管理しているリソースの所有権を移動する
とてもめずらしい実装です。
auto_ptrがこの方法で実装されています。
auto_pointer<int> a(new int);
auto_pointer<int> b(a); // aをbにコピー
// aはvoidを指すようになる
直感に反する挙動です。原則使わないほうがいいです。
項15 リソース管理クラスが管理するリソースに直接アクセスする手段を用意しよう(Provide access to raw resources in resource-managing classes)
Cで実装されたAPIはよくポインタをつかって引数を渡す関数が多々見られます。
古いライブラリに対応するために管理しているリソースに直接アクセス出来る手段を提供しましょう。
この手段には明示的な型変換を伴うものと非明示的な型変換を伴うものの2種類があります。
- 明示的な型変換を伴うもの
shared_ptrなどのスマートポインタはこの方法をとっています。
下のようなポインタを取る古い関数を使いたいとします。
void nudge(Object* object); // ポインタをとる古い関数
これにアクセスできるようにリソースマネージャはObject*を返せるようにしましょう。
class ResourceManager
{
Object* obj;
publlic:
Object* get()
{
return obj;
}
};
/* 使うとき */
nudge(resourcemanager.get());
- 非明示的な型変換を伴うもの
型変換演算子を定義して自動的に型が変わるようにします。
class ResourceManager
{
Object* obj;
publlic:
Object operator Object()
{
return obj;
}
};
/* 使うとき */
nudge(resourcemanager);
明示的な型変換を伴う方法よりもとくに工夫せずに関数に引数を渡せます。
型変換によりそのまま渡すだけで関数を呼び出せるようになります。
しかし、下のような意図しないバグの原因になります。
ResourceManager manager(new Object);
Object a = manager;
managerのデストラクタ呼ばれた時にObject aがどこも参照しない(ダングルする)可能性があります。
可能な限り明示的な型変換を伴う実装にしましょう。