テンプレートクラスを継承したクラステンプレートを作るときの罠

何回も言われていることだけれど、つまづくことが多いところなのでメモしておきます。

以下のようなコードで、コンパイルエラーになるときの対処法です。

template<class U>
class Base {
public:
    int f() { return 1; }
};

template<class T>
class Derived : Base<T> {
  int g() { return f(); }
};

g++では、error: there are no arguments to 'f' that depend on a template parameter, so a declaration of 'f' must be available [-fpermissive]というメッセージが出ます。 clang++では、error: use of undeclared identifier 'f'というメッセージが出ます。これは不親切ですね……。

それに加えて、グローバル空間にf()が定義されていた場合、何の警告も出ずにそっちに誤爆してしまいます。こういうことが起こるとかなり混乱します(デバッガでどの関数が呼ばれているかを確認できれば、バグ取り自体は簡単ですが……)。

このような現象は、以下の条件を全て満たしたとき、起こります。

  • クラステンプレートを書いている(今回は、template<class T>Derivedが該当)
  • そのクラステンプレートが、テンプレートクラス(今回は、Base<T>が該当)を継承している
  • その継承元のテンプレートクラスのテンプレート引数として、書いているクラステンプレートのテンプレート引数(今回は、Tが該当)を使っている
    • Tを使っていない場合、たとえばstd::vector<int>を継承、といった場合は該当しません
  • 継承元のテンプレートクラスの変数や関数を使っている場合
    • ただし、関数の場合、引数によっては問題ないこともあって、混乱のもととなります

解決方法

this->f()のようにthisをつける。あるいは、Base<T>::f()とする。

なぜ問題が発生するのか

  1. 何も修飾されていないf()について、コンパイラは、Derivedメンバ関数だと思って探しに行きますが、ありません。
  2. 継承元クラスのメンバ関数の可能性を検討しますが、Tが確定していない以上、継承元クラスも確定しません。よって、継承元クラスは検索されません。
  3. 順々にスコープを広げて前方宣言があるかを探していきますが、グローバル空間まで行ってもやはりありません。
  4. よってコンパイルエラーになります。

重要なのは、f()はテンプレート引数Tと無関係な風に見えるということです。 こういう場合、コンパイラTが確定していない段階でf()がどの関数に該当するのかを探しに行きます。 テンプレートの中であってもTと無関係であれば、C言語と同様、ソースコードの上の方に探しに行きます。 Baseが上の方に書いてあるから見つけられるはず、という風に考えるのは間違いです。なぜなら、TによってはBase<T>クラスが特殊化される可能性が残されているからです。

テンプレートクラスがstd::vector<int>などのように、確定する場合は、2.の部分で継承元クラスが検索されるため、問題が発生しなくなります。 特殊化される可能性が残されている点は同じですが、使った後に特殊化した場合はコンパイルエラーになります。

なぜthisなどで問題が解決するか

thisの型は、Derived<T>*です。ここには、Tの情報が含まれています。そのような「テンプレート引数に依存したもの」が含まれている場合、Tが確定してからf()がどの関数に該当するのかを探しに行きます(Two Phase Lookup)。

Base<T>::f()とする場合も、「テンプレート引数に依存したもの」が含まれているため、Tが確定してからf()がどの関数に該当するのかを探しに行きます。