この記事について
前回に引き続きScott Meyers著Effective C++第3版の解説を行っていきます。
項6 コンパイラが生成する望まない関数を明示的に使用不可にしよう(Explicitly disallow the use of compiler-generated functions you do not want.)
前回の項5で紹介したとおりコンパイラは
- コンストラクタ
- コピー代入
- デストラクタ
を自動的に生成します。
特に2番のコピー代入が問題です。インスタンスがひとつしか無い方がよろしい場合がプログラム上で多々見られるからです。
この場合はコンパイラが生成する関数をプライベートで宣言して実装しないことでコンパイラ自動的に生成する関数を使用不可にすることができます。
class Person
{
Person(); // デフォルトコンストラクタ。実装しない
Person(const Person& rhs); // コピーコンストラクタ。実装しない。
Person& operator=(const Person& rhs); // 代入演算子。実装しない。
public:
void Person(std::string name);
/* 省略 */
};
いちいち書くのがめんどくさい時はコピーができないクラスを定義して継承するという方法が考えられます。
class UnCopyable
{
protected:
UnCopyable() {}
~UnCopyable() {}
private:
UnCopyable(const UnCopyable& rhs); // 実装しない
UnCopyable& operator=(const UnCopyable% rhs); // 実装しない
};
クラスUnCopyableを継承すると継承先のクラスはコピーができなくなります。
class Person:private UnCopyable
{
public:
Person(std::string name);
/* 省略 */
};
項7 多態的な基底クラスのデストラクタは仮想関数として定義しよう (Declare destructors virtual in polymorphic base classes.)
継承先のクラスが仮想関数のデストラクタを持たない基底クラスのポインタを通してdeleteされた時の挙動は不定です。
どういうことかというと、
class Base
{
public:
~Base(); // 仮想関数ではない
};
class Child:public Base
{
public:
~Child(); // 継承先のデストラクタ
};
int main()
{
Base* p = new Base;
delete p; // 基底クラスのデストラクタが呼ばれる。
}
結果として基底クラスの部分だけがデストラクトされた奇妙なオブジェクトが発生します。
これを防ぐには基底クラスのデストラクタを仮想関数として定義します。
class Base
{
public:
virtual ~Base(); // OK
};
STLの多くのコンテナはデストラクタがvirtualで宣言されていません。
多くのC++の教科書でSTLのコンテナを継承してカスタムしたコンテナをつくる例が挙げられていますが、実際のプログラムで同じことをしてはいけません。
読者の中にはこれを読んで、
と、おもう人がいるかもしれませんがそれは間違いです。
なぜなら、仮想関数を使うとクラスのメモリのサイズが大きくなってしまうからです。
書籍内で挙げられていた例ではサイズが1.5倍から2倍も大きくなっています。
メモリのサイズを考えると必用なとき以外は仮想関数を使わないほうが賢明です。
継承先を多態的に使う(仮想関数をもつ)時のみデストラクタをvirtualで宣言!
するようにしましょう。
項8 デストラクタを中断するような例外を避けよう(Prevent exceptions from leaving destructors.)
理由はわかるかと。デストラクタの中で例外が投げられるとその時点でデストラクタの処理が中断されます。これにより、処理の終わっていないデータがリークします。
これを避けるためにはデストラクタで例外を投げる可能性のある処理をなるべく避けましょう。
どうしてもデストラクタで例外を投げうる処理をせざる負えない時は以下のように実装しましょう。
class Socket
{
bool isOpen = false; // 接続されているかどうか?
public:
void close() // 例外を投げる可能性のある処理
{
/* 閉じる処理 */
this->isOpen = false;
}
~Socket()
{
if(isOpen)
{
try {
this->close();
} catch(...) {
/* ログを取る。プログラムを強制終了する。再度実行するする。etc... */
}
return;
}
};
投げられた例外をデストラクタ内で処理できるようにしておきます。
また、別途パブリックな関数で例外を投げうる処理を呼び出せるようにしておくことで手動で呼び出せるようにしておきます。
項9 コンストラクタ、デストラクタ内で仮想関数を呼び出してはならない。(Never call virtual functions during construction or destruction)
これも意図しない動作を引き起こすため禁じ手とされています。
下のコードを見てください。
class Base
{
public:
Base()
{
init();
}
virtual void init();
};
class Child:public Base
{
public:
Child(){}
void init()
{
/* オーバライドした処理 */
}
};
Baseクラスのコンストラクタ内で仮想関数void init()を呼び出しています。
そして、Baseクラスを継承したChildクラスでvoid init()をオーバライドしています。
意図は単純ですね。継承先のクラスにあわせて初期化処理を変えたいということですね。
しかし、これは思うように動作しません。
ChildクラスのコンストラクタではBase::void init()が呼ばれます。
直感に反しますね。なぜこのようなことが起きるのでしょうか?
それは、継承されたクラスの初期化は以下のような順番で行わるからです。
- 継承元の初期化
- 継承先の初期化
1番の時点ではインスタンスは継承元のクラスのインスタンスとして扱われます。
そのため、継承元のコンストラクタの処理中では継承先のメンバは存在しないものとして扱われます。
デストラクタも順番が逆になるだけで同様です。
- 継承先の破壊
- 継承元の破壊
2の処理中は継承先のメンバは存在しないものと扱われます。
この問題に対処するにはコンストラクタ・デストラクタ内で仮想関数を使わないことです。
継承元のコンストラクタに引数を渡せることを利用して下記のように実装します。
class Base
{
public:
Base(const Data& data); // dataは初期化に必用なデータを格納する
void init(const Data& data); // dataによって処理を変える
};
class Child:public Base
{
public:
Child():Base(data);
};
項10 代入演算子が*thisの参照を返すようにしよう。(Have assignment operators return a reference to *this)
代入演算子は以下のように連鎖できます。
int a, b, c;
a=b=c=42; // a=(b=(c=42)と同じ
これは以下のようなコードで実装されています。
class Object
{
public:
Object& operator=(const Object& rhs)
{
....
return *this;
}
};
=演算子以外にも同じようい*thisの参照を返すように実装すべき演算子は他にもあります。
- +=
- -=
- /=
- *=
これは慣例なので守らなくてもコンパイルされ実行することができます。
しかし、特別な理由が無い限り慣例に従いましょう。