Effective C++

Effective C++ Section5-8
第5,6章藤川、第7,8章竹内
参考書の用語のルール
オブジェクト


ユーザ定義だけでなく、組み込み型(C++にもとからある型)の
ものでもオブジェクトと呼ぶ
シグネチャ


2
引数の型と戻り値の型のこと。C++における公式な用語では
戻り値は含まないが、この本では含めて呼ぶ。
単語の意味
extern


複数ソースコードをまたいで変数やメソッドを使用できるようにする
宣言。
deque


デックと読む。vectorと似ており、動的配列の前後に要素の挿入や
削除が可能。STLの一種。
STL


Standard Templete Library。データを格納するコンポーネントとその
コンポーネントを操作するためのアルゴリズム群であり、C++の標
準機能。dequeのほかには、vector、list、queue、stackなどがある。
explicit


3
コンストラクタが暗黙の型変換を行わないようにするための宣言。
特に理由がなければexplicit付きでコンストラクタを宣言したほうが
安全。
第5項(1)
C++が自動で書き、自動で呼び出す関数を知ろう
コンストラクタ、デストラクタ、コピー代入演算子
一つもコンストラクタを宣言しなかった場合


コンパイラが自動でデフォルトコンストラクタを生成
コピーコンストラクタ、デストラクタ、コピー代入演算子を宣言しな
かった場合


コンパイラが自動でそれらを生成

自動生成された関数はすべてpublicでinlineとなる
class Empty{};と書いても…
class Empty{
public:







Empty(){…} //デフォルトコンストラクタ
Empty(const Empty& rhs){} //コピーコンストラクタ
~Empty(){…} //デストラクタ
Empty& operator=(const Empty& ths){…} //コピー代入演算子
};
となる


4
第5項(2)
C++が自動で書き、自動で呼び出す関数を知ろう
コンストラクタ、デストラクタ、コピー代入演算子
関数の仮想性は継承される



基底クラスが仮想デストラクタを持てば、派生クラスにコンパ
イラが生成するデストラクタも仮想となる
基底クラスのデストラクタが仮想でない場合には、コンパイラ
が生成するデストラクタは仮想にはならない
コンストラクタが明示的に宣言された場合には、コンパイ
ラはデフォルトコンストラクタを生成しない


5
引数をとるコンストラクタを持つクラスを設計した場合には、引
数をとらないコンストラクタ(デフォルトコンストラクタ)を生成し
ない
第5項(3)
C++が自動で書き、自動で呼び出す関数を知ろう
コンストラクタ、デストラクタ、コピー代入演算子
コピー代入演算子の自動生成によって、C++の規則を破
りかねないようなコードの場合には、コンパイラは生成し
ない


e.g.)



6
メンバに参照変数を持ち、コード中で参照するものを変更してしまう
ような演算がある場合
constなデータメンバを持つ場合
コピー代入演算子をprivateにしているクラスの派生クラスである場合
(12項)
第6項(1)
コンパイラが自動生成することを望まない関数は、使用を禁止しよう
コンストラクタ、デストラクタ、コピー代入演算子
コピーコンストラクタとコピー代入演算子を禁止し、コン
パイルできないようにしたい場合



普通の関数であれば宣言しなければよい
コピーコンストラクタとコピー代入演算子を宣言しなければ、コ
ンパイラが自動生成してしまう
コンパイラが生成する関数はすべてpublicである


コピーコンストラクタとコピー代入演算子をprivateに記述する
ことで、コンパイラが自動生成することはなくなる

まだ安全ではない


意図的に定義を書かなければ、これらの関数を呼び出してしまう
コードを書いてもコンパイル時のリンカエラーになるだけ

7
ほかのメンバ関数やフレンド関数はprivateでも呼び出せる
iostreamなどの実装にも使われており、有名な手法である
第6項(2)
コンパイラが自動生成することを望まない関数は、使用を禁止しよう
コンストラクタ、デストラクタ、コピー代入演算子
リンカエラーをコンパイラエラーにすることもできる




メリット:エラーが早くわかる
方法:コピーコンストラクタとコピー代入演算子をprivateに宣言した
クラスを作り、このクラスを継承する
e.g.












8
class Uncopyable{
protected:
Uncopyable(){} //生成と廃棄は許可
~Uncopyable(){}
private:
//コピーと代入は禁止
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale: pricate Uncopyable{
...
};
第6項(3)
コンパイラが自動生成することを望まない関数は、使用を禁止しよう
コンストラクタ、デストラクタ、コピー代入演算子
Uncopyableの継承はpublicでなくてよい
Uncopyableのデストラクタは仮想でなくてよい




Uncopyableはデータを持っていないため、仮想でないデストラ
クタは「空の基底クラスの最適化」のため本来は望ましい
ところがUncopyableクラスは基底クラスとして設計されており、
多重継承された場合には「空のクラスの最適化」が適用され
ない可能性がある
Boostライブラリには、この働きをするnoncopyableという
クラスがある

9
第7項(1) ポリモーフィズムのための基底クラス
には仮想デストラクタを宣言しよう

時間を記録するには、いろいろな方法がある
 TimeKeeperというクラスを定義し、いろいろな時間記録クラスをその派生クラス
にしてみよう。
TimeKeeper
AtomicClock

WaterClock
WristWatch
これら派生クラスのクライアントは、時間の記録方法や計算方法の詳細を知らず
に、クラスの機能を使いたいと考える。
 ファクトリ関数が便利(???なんで?)

ファクトリ関数とは、派生クラスのオブジェクトを生成し、それを指し示す基底ク
ラスのポインタを返す関数。
第7項(2) ポリモーフィズムのための基底クラス
には仮想デストラクタを宣言しよう

基底クラスのポインタptkを使って、派生クラスのオブジェクトを操作する

ptkをdeleteすると、どうなるか?




回避するためには、基底クラスに仮想デストラクタを与える。


基底クラスTimeKeeperのデストラクタが呼び出され、その部分だけが破棄される
AtomicClockのデストラクタは呼び出されず、AtomicClockにあるデータメンバは破
棄されない
AtomicClockオブジェクトのパーツが残る
AtomicClockのデストラクタが呼び出され、オブジェクト全体が破棄されるようになる。
TimeKeeperのような基底クラスは、一般にデストラクタ以外にも仮想関数を
持っているはず

仮想関数を持つクラスは、ほぼ間違いなく仮想デストラクタを付けるべき
第7項(3) ポリモーフィズムのための基底クラス
には仮想デストラクタを宣言しよう

逆に、仮想関数のないクラスは、基底クラスとして使われるために作られたのでは
ないかもしれない

32bitのintのメンバ変数を2つ持つクラスPointを考える

Pointのオブジェクトは、64bitレジスタに収まる。

もし、Pointが仮想デストラクタを持つなら


仮想関数を持つクラスは、仮想テーブルポインタと呼ばれる「オーバーライトされた仮想関数のうち実際どれを実
行するか」の情報を持つ必要がある。

その分オブジェクトが大きくなってしまう。(ポインタは32, 64bit)

仮想デストラクタのせいで、オブジェクトサイズが50~100%増える!
基底クラスとして使うつもりがなければ、仮想デストラクタを持つべきでない
StringやVectorなどのクラスは、仮想デストラクタを持っていない



これらの派生クラスを作るのは危険。(そもそも基底クラスとして使われると想定されていない)
まとめ

ポリモーフィズムのための基底クラスには仮想デストラクタを宣言しよう。特に、クラスが仮想関数を持つなら、
仮想デストラクタも持たせよう。

基底クラスとしてデザインされていないクラスには、仮想デストラクタを宣言すべきでない
第8項(1) デストラクタから例外を投げないよう
にしよう

デストラクタから例外を投げることは、禁じられてはい
ないけど間違いなく危険



クラスWidgetのオブジェクトを10個持つVector vを考える。
Vが破棄される時、Widgetも順に破棄される。
Widgetのデストラクタに例外を投げる処理があった場合、
同時に複数のアクティブな例外が存在する状態がありう
る

その場合の振る舞いは未定

では、デストラクタが”例外を投げるかもしれない処理”
をしなければいけない場合、どうすればよいか?

.
第8項(2) デストラクタから例外を投げないよう
にしよう

では、デストラクタが”例外を投げるかもしれない処理”
をしなければいけない場合、どうすればよいか?

データベースへの接続のクラスDBConnectionを考える


DBConnectionオブジェクトを管理するクラスDBConnが
存在する



接続を切る関数void close()を持ち、失敗時は例外を投げるこ
とにする
メンバとしてDBConnectionのオブジェクトdbを持つ
デストラクタ内ではdb.close()を呼び出し、接続の切り忘れを防
ぐ
DBConnオブジェクトが破棄されると、デストラクタでデー
タベースへの接続を切ろうとする

db.close()に失敗し例外が投げられると、DBConnのデストラク
タは例外を投げ、処理が中断されてしまう
第8項(3) デストラクタから例外を投げないよう
にしよう

2つの対処法

DBConnデストラクタ中でプログラムを中止してし
まう
db.close()の例外を検出(try-catch文)し、それを記録
してstd::abort()
 それ以降処理を続けることに意味がなければ有効


DBConnデストラクタが例外を飲み込んでしまう
db.close()の例外を検出し、それを記録するだけ
 一般には良くないが、プログラムの中止や未定義な
振る舞いを防ぐためには有効

第8項(4) デストラクタから例外を投げないよう
にしよう

どちらも素晴らしい方法ではない


より良いのは、DBConnのクライアント(利用者)に”問題に対処する機
会”を与えるようなデザイン
たとえば、DBConn自身が、接続を切る関数closeを提供する






db.close()を呼ぶ責任を、DBConnのデストラクタから、DBConnのクライアン
トに移す
クライアントがcloseを呼び出せるようにするという事は、クライアントにエラー
処理の機会を与えることを意味する
例外はデストラクタ以外の関数から投げられるべき
クライアントの手で接続が切断されないままDBConnが破棄される場合は、
やむを得ずデストラクタ内でdb.close()を呼び出す(バックアップ)
中止されようが飲みこまれようが、クライアントには文句を言う筋合いはない
まとめ

デストラクタは例外を投げてはいけない。デストラクタ内で例外を投げ
る関数を呼び出す場合、その例外を補足し、処理(中止or飲みこみ)す
るようにする