この記事について
前回に引き続きScott Meyers著Effective C++の項16〜20を解説します。
項16 newとdeleteは対応するものを使おう(Use the same form in corresponding uses of new and delete)
いきなりですが問題。
下のコードの問題点は何でしょう?
std::string* sarray = new std::string[100];
delete sarray;
問題点は配列を作るnewに対してdeleteが呼び出されている点です。
正しくはdeleteの代わりにdelete[]を使う下のコードです。
std::string* sarray = new std::string[100];
delete[] sarray;
newをつかってしまうと配列が適切に解放されません。
配列のために確保された動的メモリは先頭にいくつのオブジェクトが配列内にあるかの情報を持っています。この情報に基づいてdelete[]は複数のオブジェクトを破壊します。
配列を確保するnewに対してはdelete[]を使いましょう。
逆にひとつのオブジェクトを確保するnewに対してdelete[]を使うとどうなるでしょうか。
これも問題を引き起こします。
なぜなら、delete[]は先頭のメモリに格納された情報に基づいてメモリを解放するからです。
消したいオブジェクト以外の場所も壊してしまいます。
これまでの事を前提として下のコードを見てください。
typedef Object myarray[5];
非常に危険なtypedefですね。このような配列の型を提議するtypedefを使うときはdeleteとdlete[]のどちらを呼びださなければならないかを使う人に教えなければなりません。
このようにnewは面倒の多いC++の機能です。
代わりにvectorやlistなどのコンテナを使うことを検討しましょう。
項17 newされたオブジェクトは独立したステートメントでスマートポインタに格納しよう(Store newed objects in smart pointers in standalone statements.)
次のようなコードがあるとします。
int breakEgg(); // 卵を割る関数
class Egg // クラス
{
/* 省略 */
};
tr1::shared_ptr<Egg> whip(tr1::shared_ptr<Egg>, int); // 共有スマートポインタを引数に取る関数
/* whip関数を実行 */
tr1::shared_ptr<Egg> whip(tr1::shared_ptr<Egg>(new Egg), breakEgg());
/*
一見良さそうに見えます。
しかし、このコードは見つけにくい危険をはらんでいます。
もしbreakEgg関数が例外を投げて処理を中断した場合どうなるでしょうか。
答えはnewで確保したリソースがリークします。
なぜなら上のコードは以下の順番で実行される可能性があるからです。
- new Eggを実行してリソースを確保
- breakEggを実行
- tr1::shared_ptrのコンストラクタを呼び出して確保したリソースをラップ
- whipを実行する
2番で処理が中断されるとリソースはshared_ptrでラップされません。このためdeleteが実行されずにリソースがリークします。
めんどくさがらずにリソースの確保と関数の実行を分けましょう。
tr1::shared_ptr<Egg> egg = tr1::shared_ptr(new Egg);
whip(egg, breakEgg());
項18 インターフェースを正しく使うと使いやすいように、正しくない場合は使いにくいようにいしよう(Make interfaces easy to use correctly and hard to use incorrectly)
世の中には想像もつかないようなお馬鹿さんがいます。ひとつ例にあげるとしたらそれは未来の自分です。
ドラえもんのタイムマシンに乗って過去に遡り自分をとっちめてやりたくなったらあなたはこのルールを守らなかったということです。
例えば日付を処理する関数あったとします。
void processDate(int year, int month, int day);
int型で年、月、日を順に与えていますが、未来の自分は急に外国にかぶれだして月、日、年に渡し始めるかもしれません。
これを年、月、日を定義するクラスや構造体を作ったほうが間違いようがなくなります。
void processDate(const Year& year, const Month& month, const Day& day);
なぜなら、このようにコーディングすれば順番を間違えた時にコンパイルを通らなくなるからです。そのうえ、Year,Month,Day各クラスの値の設定時に設定する値の正当性をチェックすることが出来るようにできます。
このように間違えた使い方をした場合にコンパイルできなくする方法が多くあります。
他の項で紹介しているのでそちらを見てみてください。
項19 クラスの設計を型の設計として捉えよう(Treat class design as type design.)
クラスの設計は難しいです。項19ではクラスの設計をする際に考慮すべきポイントを紹介しています。
- クラスはどのように生成され、破壊されるか?
- クラスの初期化と代入はどのように異なるか?
- 値として渡されたときの振る舞いをどうするか?
- クラスにとって妥当な値はなにか?
- 継承の構造にどのように当てはまるか?
- どのような型変換がゆるされるか?
- どのような演算子と関数が適しているか?
- どのコンパイラが自動生成する関数を禁止するか?
- 何がプライベートメンバにアクセスできるようにするか?
- 使用できないようにするインターフェースは何か?
- どのくらいクラスは汎用的か?
- クラスは本当に必要なものなのか?
残念ながら私は深く解説出来るほどの知識を持っていません。
誰か解説してくれませんか?(丸投げ)
項20 値渡しよりも定数参照渡しを使おう。(Prefer pass-by-reference-to-const to pass-by-value.)
C++はCから引き継がれた機能として関数の引数をデフォルトで値渡しとして扱います。
下のコードを見てください。
void punch(Person taro);
太郎くんがパンチされているように見えます。
しかし、実際は違います。値渡しなので実際に殴られるのは太郎くんのコピーです。そして、関数を出る際にそのコピーはデストラクタによって死にます。
話はここで終わりません。コピーを作成するときにコピーコンストラクタが呼ばれます。
Personクラスが継承を用いていた場合、継承元のコンストラクタがよばれます。また、メンバにクラスがあった場合メンバのコンストラクタもよばれます。
絵で表すとこのような感じ…
カオスですね。
このような不必要なコピーを避けるために定数参照で引数を渡しましょう。
void punch(const Person& taro);
定数参照で渡す他の利点としてslicing problemの回避が挙げられます。
Personクラスの継承元にMonkyクラスがあったとします。
ここで、Monkyを引数にとる関数があるとします。
void eatBanana(Monky saru);
この関数にPersonのインスタンスのtaroを渡すとどうなるでしょうか?
Monkyのコピーコンストラクタが呼ばれるため関数内ではtaroの人としての部分は失われて猿として振る舞うように成ります。ウキー!!
定数参照として渡せばコピーコンストラクタが呼ばれないのでこの問題を回避できます。
では、すべての引数を定数参照渡しにすればいいのでしょうか?
処理速度を考えるとそれは良くない考えです。
なぜなら、データサイズの小さいintやcharなどの場合コピーしたほうが参照で渡すよりも速い場合があるからです。
参照はポインタとして実装されています。このためポインタの指し示す先のメモリに対するアクセスを繰り返さなければなりません。コピーならばメモリにアクセスすること無くレジスタ間のデータのやり取りで処理を終わらせることが出来ます。
しかし、このハックは実行環境、コンパイラ、ライブラリによって大きく左右されます。
移植性を失う上に、達人レベルの知識が要求されます。