項26 可能な限り変数の宣言を先延ばしにしよう(Postpone variable definitions as long as possible.)
下のコードの無駄は何でしょうか?
Object object;
Object tweaked_object;
tweaked_object = tweak(object);
正解はtweaked_objectが必要のないデフォルトコンストラクタを呼んで初期化している点です。
よりよいコードは以下です。
Object object;
Object tweaked_object = tweak(object);
このように変数を使う直前で宣言するとプログラムの無駄を減らすことができます。
さらに可読性もあげることが出来ます。
プログラムを読んでいてこんなことはありませんか?
/* 長いコード */
Person old_women;
old_women.speakTo(spanish_inquisition); // who is that!?
/* 長いコード */
いきなりどこで宣言されてどのような処理をしたのか分からない変数が現れます。
読んでいてものすごく混乱するコードです。宣言を遅らせることで変数の出自や経歴がわかりやすくなり読みやすいコードになります。
forやwhileなどのループでは状況が異なります。
Object object;
for(int i = 0; i < 20; ++i)
{
/* 変数objectをつかった処理 */
}
このコードではobject変数の初期化は一回だけ行われます。一見良いコードのようですが、objectがループ内でしか使われない場合必要のない範囲まで変数objectのスコープが及んでしまいます。
Objectの初期化が重くないのならばできるだけ以下のように書きましょう。
for(int i = 0; i < 20; ++i)
{
Object object;
/* 変数objectを使った処理 */
}
項27 キャストをできるだけ使わないこと。(Minimize casting)
C++はCスタイルのキャストとC++スタイルのキャストの2種類のキャストをサポートしています。
C++のキャストは役割によって細分化されています。
-
static_cast(expression)
一番よく見るキャストです。非明示的な変換を強制的に行います。
intをdoubleにしたり、voidをcharにしたりします。 -
const_cast(expression)
const修飾子を外すのに使います。 -
dynamic_cast(expressionn)
継承元のポインタを継承先のポインタに変換する際に使われます。継承先のクラスを継承元のクラスに変換することが出来ます。実行時のコストが非常に高いことで知られています。 -
reinterpret_cast(expression)
ポインタをintに変換するなど異なる型動詞の変換に使われます。
C++のキャストを使うことでキャスト時の意図を正確に伝えられるようになります。
キャストの難しい点は一見正しいけど間違っているコードを簡単に書けてしまう点です。
Derived derived;
Base *pbase = &derived;
assert(pbase == &derived);
恐ろしいことにDerivedからBaseに変換されるときにポインタの指している先は変わってしまいます。これは継承の機能をサポートするためにメモリにファッジを入れるからです。
Cから移行してきたひとがよくする間違いです。
次のコードを見てみましょう。
class Derived : public Base
{
pbulic:
virtual void punch()
{
static_cast<Base>(*this).punch();
return;
}
};
コードの意図はわかるかと。基底クラスのメソッドと同じ内容を繰り返しかくのが面倒くさいから継承先で基底クラスの関数を呼びだそうとしています。
確かに基底クラスのメソッドは呼ばれます。問題なのは*thisに対してではなく基底クラスの新しいインスタンスに対して呼ばれます。
上のコードは下のように書くと正しく動作します。
class Derived : public Base
{
public:
virtual void punch()
{
Base::punch();
return;
}
};
dynamic_castはクラスの名前をひとつづつ比較して変換すべき型を見つけでしてから変換を行います。そのため先述したようにコストが大きいです。使用を避けましょう。
dynamic_castを使用したい衝動にかられるのは以下のような時です。
if(Derived* pderive = dynamic_cast<Derived*>(pobject))
pderive->punch();
このような衝動に駆られたらvirtualを使いましょう。すべてではありませんが仮想関数により多くのdynamic_castをコードから取り除くことができます。
項28 オブジェクト内部のハンドルを返すのを避けよう(Avoid returning "handles" to object internals)
オブジェクとの内部のハンドルを返すというのは以下のようなコードです。
class Object
{
Data data_;
public:
Data& getHandle()
{
return &data_;
}
};
Object object;
Data& a = object.getHandle();
何が問題かは明らかですね。カプセル化が全くされていないです。ハンドルを通してプライベートの要素を好き勝手出来ます。しかも、インスタンスが破壊されるとaはどこも指していない状態,すなわちダングリングします。
項29 コードが例外安全であるように努めよう(Strive for exception-safe code)
コードは例外安全であるか、または例外安全でないという2つの状態しかありません。
部分的に例外安全ということはありえずコードのどこかが例外安全でないならばコード全体が例外安全でなくなります。
例外安全な関数は以下の要件を満たします。
- リソースの漏れが無いこと。
- データが破壊されないこと。
- 例外を一切投げないこと。
1番が特に厄介なものです。メモリリークやデッドロックの原因となるからです。
これに対処するにはリソースを管理するクラスstd::shared_ptrやstd::lockを使うことが挙げられます。例外が投げられた際には例外が投げられたスコープのオブジェクトはデストラクタが呼ばれて破壊されます。この事を利用して例外が呼ばれた時にメモリを開放したりロックを解除します。
項30 inlineの長所と短所を理解しよう(Understand the ins and outs of inlining)
inline関数はマクロの代わりとしてC++に導入された機能です。一見普通の関数と同じに見えますが関数を呼び出す際に通常形成されるスタックフレームがありません。このため、関数を頻繁に呼び出すよりも高速に実行できます。
このことはアセンブラのコードを見ればわかります。
inline関数を呼び出しているときはしたのようなスタック形成のコードがありません。
(最近はレジスタを通じて値を渡すので以下のようなコードが見られない場合もあります。)
push eax
push ebx
push ecx
;処理
pop ecx
pop ebx
pop eax
ret
ひとつ注意すべきなのはinline修飾しが突いた関数は必ずinlineになるわけではないということです。ポインタを通じて呼び出される関数の場合は通常inline化されません。
インライン化された関数はコンパイル時に実体が生成されます。そのため、通常はヘッダファイルでインライン関数を定義します。このことはインライン関数をよびだす関数のオブジェクトコードのサイズを増大させます。ライブラリを作る場合は外部から呼び出される関数はインラインで定義しないようにしましょう。
また、コンストラクタ・デストラクタをインラインで定義するのも良くない判断です。なぜなら、一見空っぽに見える下記のコンストラクタは
class AwesomeObject : Object
{
/* 省略 */
public:
AwesomeObject()
{}
/* 省略 */
};
実際はコレだけ多くのことをしています。
class AwesomeObject : Object
{
/* 省略 */
public:
AwesomeObject()
{
Object::Objecct(); // 基底クラスの初期化
try {
/* 基底クラス各メンバ変数の初期化 */
} catch {
Object::~Object();
}
/* AwesomeObjectクラスのメンバ変数の初期化 */
}
/* 省略 */
};
デストラクタも同様です。コンストラクタ・デストラクタをinline化するとオブジェクトコードのサイズとクラスを利用する関数のコンパイル時間は極端に肥大化します。
コンストラクタとデストラクタをinline化するのは良くない考えです。