なぜ識別子がみつからなかった?
冒頭のコードを再掲しましょう。
template <typename T>
struct BASE
{
int member;
};
template <typename T>
struct DERIVED : BASE<T>
{
void foo()
{
member = 100; // <=== ※1
}
};
この member は BASE<T> のデータメンバです。BASE<T> は T に依存するので T が決まるまでは member は何かわかりません。
でも、BASE<T> の定義は見えているわけだから、 member が BASE<T> のデータメンバだとわかるのでは?
と思われるかもしれません。しかし T が決定するまでは油断はできません。
BASE<T> のプライマリテンプレートでは確かにそのとおりですが、T が何かの型に例えば int にバインドされたとき、BASE<int> の明示的特殊化が後で宣言される可能性があるためです。
このプライマリテンプレート、そして特殊化とはなんでしょうか?
プライマリテンプレートと特殊化
{クラス/関数}テンプレートには特殊化という魔法があります。
役所の手続きを考えましょう。普通は申請書と本人確認書類の確認で何かの手続きができるものとします。これが通常のルートです。
しかし、未成年が申請者だったときには、保護者の同意書や他の書類と何かの審査が増えるとします。
この場合は、役所では成年の通常ルートとは別の未成年用の対応ルートを設ける必要があります。
テンプレートもこのような対応が可能です。関数テンプレートの書式に従った疑似コードは以下のようになります。
template <typename 人>
void 申請手続き( const 人& )
{
申請書と本人確認の手続き
}
template <>
void 申請手続き<未成年>( const 未成年& )
{
保護者および学校の同意の確認
申請書と本人確認の手続き
}
このときの、
template <typename 人>
void 申請手続き( const 人& ){}
はプライマリテンプレートといいます。
これは特殊化が用意されていないときの標準の実装となります。
そして、未成年がパラメータに与えられたときに、それに特化した
template <>
void 申請手続き<未成年>( const 未成年& ){}
という処理を書くことができます。これを特殊化といいます。
関数テンプレートは
- プライマリテンプレート
- 完全特殊化
2段構えですが、
クラステンプレートの場合は
- プライマリテンプレート
- 部分特殊化
- 完全特殊化
の3段構えになります。
部分特殊化という中途半端でチートな特殊化があるのです。
ちなみに if constexpr () の説明で出てきた
// 非ポインタ版(通常の型)
template <typename T>
void print_value(T v){}
// ポインタ版(オーバーロード)
template <typename T>
void print_value(T* v){}
という例も通常版とポインタ版の特殊化と思われるかもしれませんが、これはどちらもプライマリテンプレートで、テンプレートのオーバーロードを選択するものです。
微妙に書式が違うことに注意してみてください。
さて標準ライブラリには、おなじみの std::vector<T> がありますが、この T が bool にバインドされた std::vector<bool> となると
特別な実装が用意されています。
bool のサイズは通常は1バイトですが、std::vector<bool> の内部では 1bit で保持します。operator[] は bool& の代理となる実装定義な型を返すことで同じ動作ができるようになります。しかし、内部ではビット単位でその内容を保持しているため、bool 型の配列のアドレスを取得することはできません。
テンプレート特殊化は応用範囲が広く、奥も深いので近いうちに取り上げる予定なので乞うご期待です。
このテンプレート特殊化が、T が確定するまでは member が BASE のメンバであると決定できないという理由です。
ちなみに非依存名にすると
BASE<T>をクラステンプレートではなくて、ただのクラス(構造体)にしてみると、
- BASE という構造体
- その member というデータメンバ
は T には依存しない非依存名になり、あっさりとコンパイルは通るようになります。
struct BASE
{
int member;
};
template <typename T>
struct DERIVED : BASE
{
void foo()
{
member = 100; // OK
}
};
ではどう書くのが正解だった?
依存名 member の話にもどります。
基底クラス依存の member を解決するには以下のように書きます。
template <typename T>
struct BASE
{
int member;
};
template <typename T>
struct DERIVED : BASE<T>
{
void foo()
{
this->member = 100; // <=== ※1
}
};
this-> をつけると member はまだ確定していないけれど、基底クラスにあるに違いないので member は依存名として解決を後回しにしようと第一フェーズを通過できます。
しかし、私は以下のようなミスをしていました。
template <typename T>
struct BASE
{
virtual void func()const {
std::cout << "BASE::func()\n";
};
};
template <typename T>
struct DERIVED : BASE<T>
{
void func() const override {
std::cout << "DERIVED::func()\n";
};
void func()const override {};
void foo()
{
// func(); と書くとコンパイルが通らなかったので
// ↓ こうした。
BASE<T>::func();
}
};
基底クラスのメンバなのでスコープを解決できればいいのかと思って、基底クラスへのスコープ解決演算子を使ったのです。
これ、どうなるかわかりますよね。仮想関数の場合はこれで動的束縛が効かなくなるのです。
これは実際に動かして、基底クラスの仮想関数が呼ばれるという些細な動作の違いを見つけるまで私はミスに気づきませんでした。
(つづく)