注目しているコード上では例外が投げられる可能性があることが明示的には見えないのがこの問題を見つけにくくしていると思います。
ただ、今回のようなカスタムでメモリ確保と初期化を行う必要がある場合は少ないですし、適切なデストラクタを持っているメンバ変数ならばまず問題ありません。
逆にいえば、生ポインタのメンバ変数は、リークの回避が難しいのでご注意ください。
上記の例は大丈夫ですが、例えば A
のコンストラクタ内で例外を飛ばし得るコードを追加してしまうと、例外が飛んだとき A
のデストラクタは呼ばれず、リークしてしまいますので、コンストラクタ内で適切に処理する必要があります。
また、別のメンバ変数がもし存在し、a_
より後に初期化しようとしたときに例外が飛んだ場合は、それを自分でキャッチして a_
を手動で開放するなどの手当が必要になり、かなり面倒で読みづらいコードになると思います。
自分でこのようなリソース管理のためのラッパークラスを作る場合は、コンストラクタでの例外処理に気をつかいましょう。
異なる型だけれど、それらが同一のシグネチャを持つメンバ関数を持つことを保証させる機能をインターフェースといいます。 これまで説明した継承による多態を使ってインターフェースとその実装を分離することができます。 異なる型が同じインターフェースを要求するものとして、テンプレートがあります。テンプレート型引数は、同じインターフェースを持っていないと同じテンプレートコードを具現化できません。これもインターフェースの一種です。 ただし、テンプレートは静的に型を解決するので、使う型の数だけコードが増えるというデメリットもあります。
演算子オーバーロードとは、演算子の実装を自由に定義できる機能です。
演算子といってもたくさんあるので、用途に応じて独自の定義をする演算子は限られるでしょう。
ここではクラス T
のメンバ関数として定義する演算子の代表的なものを挙げておきます。
bool operator==(const T&) const
bool operator!=(const T&) const
bool operator<(const T&) const
bool opeator>(const T&) const
bool opeator<=(const T&) const
bool operator>=(const T&) const
- オブジェクトの一致性や順序性を表すのに使えます。
- 最近のコンテナやアルゴリズムは、比較のための関数オブジェクトを受けつけることが多いので、必ずしもクラスの側に比較演算子を実装する必要はないことが多いです。
- 一致性を意味する
==
と!=
は、同時に定義し、一貫性が損われないようにしましょう。 例えば、必ずbool operator!=(const T& rhs) const { return !operator==(rhs); }
としておくなどです。 - 順序性
<
<=
>
>=
についても同様です。 - 比較演算子を含む一部の演算子は、グローバル関数としても定義できます。
例えば
bool operator==(const T& lhs, const T& rhs)
と、クラスT
内で定義するbool operator==(const T& rhs)
は同じシグネチャとして扱われ、同時には定義できません。
T& operator=(const T&)
T& operator=(T&&)
- オブジェクトのコピーやムーヴを実装したいときに定義します。
- コピーコンストラクタやムーヴコンストラクタと一緒に定義されることが多いです。 コピーのみ、ムーヴのみサポートするクラスもありますし、両方サポートするクラスもあります。
... operator()(...)
- 関数オブジェクトとして振る舞うときの挙動を定義できます。
引数や返り値の型は自由に決められます。
例えば
int
とfloat
を受けとり、int
型を返すファンクタはint operator()(int, float)
と書きます。 - 関数オブジェクトとして振る舞うことを求められているとき以外は、きちんと名前を付けて通常のメンバ関数として呼ぶべきです。 名前を付けるのが面倒くさいという理由でファンクタにしてはいけません。 可読性が落ちます。
T& operator[](size_t)
const T& operator[](size_t) const
- 要素へのアクセスを実現するために定義します。
std::vector
やstd::string
が持っていますね。
たくさんあるので必要に応じて自分で調べてください。
引数の異なる型ごとに同じ演算子を定義する、すなわちオーバーロードすることで挙動を変えることができます。 ただし、実体が選ばれる条件が複雑になってしまい、期待と異なる実態が呼ばれてしまうなど間違えやすくなるようならオーバーロードは諦め、異なる名前をつけたり、異なるクラスに分離したりするなどしてメンテナンス性を損わないようにしましょう。
コンテナで使うことが前提の場合は、自分で作ったカスタムメモリアロケータを std::vector
などのテンプレート引数に渡して使います。
struct Allocator
{
using value_type = A;
A* allocate(size_t n) {
void *p;
if (::posix_memalign(&p, ALIGNMENT, sizeof(A) * n) != 0) {
throw std::bad_alloc();
}
return p;
}
void deallocate(A* p, size_t) {
::free(p);
}
};
int main()
{
std::vector<A, Allocator> v;
v.emplace_back();
}
ただし、上記の実装は、std::vector
の場合は連続メモリを確保してしまうので、
先頭要素だけアラインされたものになる可能性があることに注意してください。
これを避けるには、sizeof(A)
も ALIGNMENT
の倍数にする必要があります。
参照の寿命が、それが指しているオブジェクトの寿命と同じになるとは限りません。 オブジェクトの寿命が参照より先に来てしまうことはあり得ます。 そのような状況下で参照にアクセスすると、不定な動作を引き起こすでしょう。
A& f()
{
A a;
return a;
}
このコードはコンパイルできますが、明らかに変数 a
の寿命は f()
が完了した時点で尽きています。
それなのに、a
の参照を返してしまっているので、明らかに返り値は正しくない(a
のデストラクタが呼ばれた後の、他の用途に再利用されたかも知れない)メモリ領域を指しています。
つまりバグです。