この記事について
前回に引き続きScott Meyers著Effective C++の項21〜25を紹介します。
項21 オブジェクトを返すべきときに参照を返してはならない。(Don't try to return a reference when you must return an object.)
前回で紹介した参照渡しを知った人の中には偏執的に参照渡しを多用するようになる人がいます。
しかし、参照渡しの乱用は大きな問題を産みます。
Object& nudge(const Object& object)
{
/* いろいろな処理 */
return modified_object;
}
一見正当なコードに見えます。
しかし、この関数は不当です。
なぜなら、modified_objectは関数の処理が終わるとともになくなってしまうからです。
これにより、返り値のObject&は何もない場所をさす参照になってしまいます。
これを回避するひとつの方法としてnewをつかって動的にオブジェクトを生成する方法が挙げられます。
Object& nudge(const Object& object)
{
Object* modified_object = new Object;
/* いろいろな処理 */
return modified_object;
}
これで大団円かと思いきやそうも行きません。newされたオブジェクトはdeleteされなければなりません。誰が生成されたオブジェクトをdeleteするのでしょうか?
特に下のようなコードを書いた場合、生成されたオブジェクトは必ずリークします。
Object& object = nudge(nudge(a));
他にもstatic変数や配列をつかった方法がありますがどれも固有の問題を抱えています。
関数内で生成されたオブジェクトを参照で返すのはやめましょう。
コンストラクタを呼ぶ際の僅かなロスとコード全体の安全を交換する道理はどこにもありません。
項22 データメンバをプライベートで宣言せよ。(Declare data members private.)
データメンバをプライベートで宣言する第一の利点はデータに対するアクセス制限をかけることができるということです。
書き込み、読み込みのいずれか、または両方を制限することでクラスのカプセル化を強化します。
以下のコードがアクセス制限の実装例です。
class AccessControl
{
Data data_; // 大切なデータ
public:
Data readOnly() const
{
return data_;
}
void writeOnly(const Data& data)
{
data_ = data;
return;
}
};
このクラスでreadOnlyメンバ関数のみを実装すればdata_は読み込みだけ出来るようになります。
writeOnlyのみを実装すれば書き込みだけを実現できます。
そして、両方実装すれば読み書き。両方の関数を実装しなければ読み書きを禁止できます。
このようにデータメンバをプライベートにすることでキメの細かいアクセス制限をかけることができます。
他にもクラスが変更に対して強くなります。
メンバ関数を介してのアクセスによりクラスの利用するほうのコードに影響を与えずに変更を加えることが可能です。
例えば、Dataクラスよりも高速に演算を出来るSuperDataクラスができたとしましょう。
このクラスを先ほどのAccessControlクラスにインターフェースを変えずに埋め込むことができます。
class AccessControl
{
SuperData super_data_; // 改良されたデータ
public:
Data readOnly() const
{
return static_cast<Data>(super_data_);
}
void writeOnly(const Data& data)
{
super_data_ = static_cast<SuperData>(data);
return;
}
};
こうすることでクラスの利用者に影響を与えずにリファクタリング・保守が可能となります。
書いたコードを延命させるためにデータメンバはprivateで宣言しましょう。
項23 メンバ関数よりも非メンバ関数を使おう。(Prefer non-member non-friend functions to member functions)
オブジェクト指向プログラミングの肝は
データとそのデータに対する操作は一箇所に集めるべき
と、説いています。
なので、初心者がよく犯す間違いとして以下のようなクラスが挙げられます。
class MyFirstObject
{
Data data_;
public:
void dataManipulationA();
void dataManipulationB();
void dataManipulationC();
void dataManipulationD();
/* 延々とコレがつづく */
};
これは間違ったオブジェクト指向の捉え方に基づいています。
オブジェクト指向で大切なのはなぜ
データとそのデータに対する操作は一箇所に集めるべき
なのかを理解することです。
一箇所に集めるべき理由はただひとつです。
*データをカプセル化して直接的なアクセスを制限するため
これを理解していないと上のようなコードが出来てしまいます。
クラスメンバが増えるとデータに直接アクセスできる関数が増えてしまいます。
このためデータのモジュール強度は下がってしまいます。
上のコードを理想的なオブジェクト指向のクラスに書き直すにはSTLを参考にしましょう。
STLのクラスは最小限のメンバ関数を使っています。
特殊な操作は関数としてalgrithm.hppなど別のヘッダに定義されています。使用するときのみ必用なヘッダを読み込み使用します。
クラスと関数は名前空間を通じて結び付けられています。こうすることにより、不要な関数はコンパイラに読み込まれないため実行ファイルのサイズを小さくすることが出来ます。
この方針に基づいて上のコードを書き換えるとこのように成ります。
class Object
{
Data data_;
public:
Data readData();
void writeData(const Data& data);
};
void manipulateDataA(Data& data);
void manipulateDataB(Data& data);
/* 省略 */
項24 型変換をすべての引数に適用すべき関数を定義するときは非メンバ関数として定義しよう(Declare non-member functions when type conversions should apply to all parameters.)
型変換をすべての引数に適用すべき関数の一例には演算子が挙げられます。
例えば、演算子を定義刷る際にメンバ関数として定義したとします。
class Object
{
public:
const Object operator+(const Object& other);
};
これはObjectのインスタンス動詞の足し算ならば問題ありません。
しかし、他の型と足し算したくなった場合に大きな問題が発生します。
それは、交換法則が成り立たなくなってしまうことです。
Object object;
object = object + 2; // OK
object = 2 + object + 2; // NG
交換法則を成り立たせるには演算子を非メンバ関数にするしかありません。
class Object
{
public:
Object(const int& num); // 型変換を定義
};
const Object operator+(const Object& lhs, const Object& rhs);
これにより、交換法則の成り立つ演算子が定義できます。
項25 例外を投げないswapを実装することを検討しよう(Consider support for a non-throwing swap)
STLのswapは以下のように実装されています。
namespace std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
return;
}
}
基本的にはstd::swapを使えばことは足ります。
しかし、中には特殊な実装を使わなければいけないクラスもあります。
そのようなクラスとして挙げられるのはpimplイディオムをつかったクラスです。
pimplイディオムはインターフェースと実装を完全に隔離するために使われるイディオムです。
簡単に言えばprivate属性のもっとすごい版。これをやるとライブラリの利用者に完全に情報を隠すことが出来るようになります。
pimplは以下のように実装されます。
class Object
{
/* 省略 */
private:
ObjectImpl* pimpl;
};
このpimplをスワップするにはObject全体を交換する必要はありません。
pimplメンバが指し示している先を変えるだけで良いです。
std::swapをObjectのために特殊化しましょう。
namespace std
{
template<>
void swap<Object>(Object& lhs, Object& rhs)
{
std::swap(a.pimpl, b.pimpl);
return;
}
}
このコードは大切なことを忘れています。pimplはプライベートメンバなのでアクセスできないことです。friend関数を使うのがひとつの手ですがfriend関数はオブジェクトのカプセル強度を下げてしまいます。ここでの良い対応策はswapメソッドを使うことです。
class Object
{
ObjectImpl* pimpl;
public:
void swap(Object& other_pimpl);
};
namespace std
{
template<>
void swap<Object>(Object& lhs, Object& rhs)
{
a.swap(b);
return;
}
}
このコードはコンパイル出来る上にstd::swapと互換性がありどこからでもアクセスできます。
今回の肝はstd::swapの拡張です。
名前空間は後から自由に拡張できます。しかも、熟練したプログラマならばレファレンスを読まずに関数を呼び出せます。開発しやすさを向上させるために似た機能がstdの中にあるならば積極的にstdを拡張していきましょう。(行き過ぎない程度に)