はじめに
静的 Template Method パターンの実装に利用されるCRTP。
抽象クラスで実装した場合のオーバーヘッドがなくなるところがとても素敵だと思います。
それが楽しくて、馬鹿の一つ覚えのようにCRTPを使っていたのですが、基底クラスコンストラクタ中で派生クラスのメンバ関数呼び出しが含まれる処理を書き、ビルドして実行したところ、未定義領域へのアクセス違反の例外が投げられました。
当時は原因が全く分からず、悲しみに暮れてました。
原因はすごくくだらないことでしたが、備忘録として。
MSVCのC++14以降でコンパイルしてます。
原因
問題の派生クラスのメンバ関数中で、派生クラスで定義されているポインタメンバ変数の中身へのアクセスが含まれていたためでした。
基底クラスのコンストラクタ実行中は、まだ派生クラスのメンバ変数の初期化が行われていないはずですから、そりゃあ未定義ですよね。
なお、基底クラスのコンストラクタ中に、派生クラスで定義されている非ポインタのメンバ変数へのアクセスが含まれる場合はより危険です。
未初期化のでたらめな値を読み取りながらも、例外は送出されず、何事もなかったかのように処理を継続します。
派生クラスのメンバ変数へのアクセスが含まれなければ、基底クラスにおけるメンバ関数の呼び出しは問題なく動作します。
再現コード
Base::hoge()
でポインタメンバ変数の中身へアクセスしているFuga::hogehoge()
が呼び出されています。
そのBase::hoge()
をBaseコンストラクタ中で呼び出したとき例外が送出されます。
// 基底クラス
template <class Derived>
class Base
{
public:
Base()
{
int num = this->hoge(); // NG
}
int hoge()
{
// 派生クラスのメンバ関数を呼び出している
return static_cast<Derived&>(*this).hogehoge();
}
};
// 派生クラス
class Fuga : public Base<Fuga>
{
public:
Fuga() : member(new int(5)) {}
~Fuga() { delete this->member; }
int hogehoge()
{
// Baseコンストラクタからの呼び出しの場合、ここでアクセス違反発生
return *this->member;
}
// ポインタメンバ変数
int* member = nullptr;
};
int main()
{
Fuga fuga;
int num = fuga.hoge(); // OK
return 0;
}
Fuga::member
が非ポインタの場合
下記は非ポインタメンバ変数の場合です。
Fuga::member
は宣言時に0初期化しているのでBaseコンストラクタ中のnum
の値も0になるように見えますが、おそらくでたらめな値が入っています。
// 基底クラス
template <class Derived>
class Base
{
public:
Base()
{
int num = this->hoge(); // numにはでたらめな値が入るが、そのまま処理が流れる
}
int hoge()
{
// 派生クラスのメンバ関数を呼び出している
return static_cast<Derived&>(*this).hogehoge();
}
};
// 派生クラス
class Fuga : public Base<Fuga>
{
public:
int hogehoge()
{
// 非ポインタなので例外は発生しないが...
return this->member;
}
// 非ポインタメンバ変数
int member = 0;
};
int main()
{
Fuga fuga;
int num = fuga.hoge(); // OK
return 0;
}
あとがき
どこに派生クラスのメンバ変数へのアクセスが紛れ込んでいるかもわからないので、場合によってはCRTP利用時の基底クラスコンストラクタ中でのメンバ変数呼び出しを、すべて禁止するのもありな気がしました。
往々にして当たり前のことほど考慮から抜けてしまい、原因の究明時に気づかないものですね。