テンプレートの派生クラスから親クラスのメンバへのアクセス

TL;DR

テンプレートクラスを派生したクラスから、親クラスのメンバを参照するコードをg++でコンパイルしようとして、以下のようなエラーが出たら、エラーが出ているメンバをthis->で修飾すること。

test.cpp: In member function 'void Derived<T>::hogehoge()':
test.cpp:13:8: error: there are no arguments to 'hoge' that depend on a template parameter, so a declaration of 'hoge' must be available [-fpermissive]
   hoge();
        ^
test.cpp:13:8: note: (if you use '-fpermissive', G++ will accept your code, but allowing the use of an undeclared name is deprecated)
  • Q. なぜエラーが起きるの?
  • A. テンプレートを含む場合の名前解決が通常の場合と異なるから。
  • Q. なぜthis->をつけるとコンパイルできるの?
  • A. シンボル名がテンプレートパラメタに依存するようになるため、名前解決が実体化時に行われるようになるから。
  • Q. なぜテンプレート派生クラスから親クラスのメンバを参照する時にthis->をつけなくてもコンパイルできる時があるの?
  • A. メソッドの引数等にテンプレートパラメタが含まれていると、名前解決が実体化時に行われ、それは通常の名前解決と同じように行われるから。

詳細については「テンプレート 名前解決」でググること。

この記事について

テンプレートの派生クラスから親クラスのメンバにアクセスしようとして、二段階名前解決(Two Phase Lookup)の仕様にひっかかってコンパイラに怒られるというのは、そうと知っていればFAQっぽいんだけれども1、テンプレートに慣れていないプログラマ(例えば俺)だと、エラーメッセージだけからはなぜ怒られているのか理解できないし、エラーメッセージでググっても、「this->usingで修飾しろ」という解決策は書いてあっても、これが「テンプレートの名前解決の問題」であり、「なぜ怒られるか」を理解するのには時間がかかる(かかった)。というわけで、なぜか検索順位の高いQiitaの記事にしておくことで、エラーメッセージでググった人に、有用なキーワードや有用なページへのリンクが見つかるようにするための踏み台記事のつもり2

何が起きるか?

テンプレートクラスを派生させ、その派生クラスから親クラスのメンバにアクセスしようとするとコンパイラに怒られる。こんな感じ。

#include <iostream>

template <class T>
struct Base {
  void hoge(void){
    std::cout << "Hoge" << std::endl;
  }
};

template <class T>
struct Derived : public Base <T> {
  void hogehoge(void){
    hoge(); // (1)
  }
};

int
main(void){
  Derived<int> d;
  d.hogehoge();
}

これをg++でコンパイルすると、

$ g++ test.cpp
test.cpp: In member function 'void Derived<T>::hogehoge()':
test.cpp:13:8: error: there are no arguments to 'hoge' that depend on a template parameter, so a declaration of 'hoge' must be available [-fpermissive]
   hoge();
        ^
test.cpp:13:8: note: (if you use '-fpermissive', G++ will accept your code, but allowing the use of an undeclared name is deprecated)

と怒られる。

どういう時に起きるか?

典型的には、既存のテンプレートライブラリから派生したクラスを作って、親クラスのメンバ関数を呼んだ時だと思う。例えばこんなの。

#include <iostream>
#include <vector>

template <class T>
class myvector : public std::vector<T> {
  public:
    void say_size(void){
      std::cout << "My size is " << size() << std::endl; // ←ここで怒られる
    }
};

int
main(void){
  myvector<int> v;
  v.say_size();
}

どうしてエラーが出るのか?

これは、テンプレートを含むコードの名前解決が二段階で行われるから。まず、シンボル名は、テンプレートパラメタに依存する名前(a name dependent on template parameters)と、依存しない名前(a non-dependent name)に区別される。その上で、

  • テンプレートパラメタに依存しない名前の解決は、定義時に行われる
  • テンプレートパラメタに依存する名前の解決は、実体化時まで遅延される

という二段階の名前解決(Two Phase Lookup)が行われる。

いま、コードtest.cppの(1)の部分の hoge()という関数は、プログラマ的には親クラスのメンバを呼んでいるつもりなのだが、コンパイラにとってはテンプレートに全く依存していない(引数などにテンプレートパラメタを含まない)シンボルであるため、親クラスの名前空間を探しにいかず、この例ではグローバル名前空間しか探さない。この時点においてグローバル名前空間にhoge()という関数は存在しないため、エラーとなる。他のコンパイラ、例えばclang++ だと、

test.cpp:13:3: error: use of undeclared identifier 'hoge'
                hoge();
                ^
1 error generated.

と、単に「hogeは見つからないよ」とだけ言ってくる。

さて、問題のメンバをthis->で修飾する。

template <class T>
struct Derived : public Base <T> {
  void hogehoge(void){
    this->hoge(); // (1)
  }
};

int
main(void){
  Derived<int> d; // (2)
  d.hogehoge();
}

ここでthisDerived<T>*の型を持っているから、this->修飾されたhoge()はテンプレートパラメタに依存するシンボルとなるため、名前解決はテンプレートの実体化の時(2)まで棚上げされる。実体化する時には、親テンプレートクラスも実体化しているので、通常の親クラス、派生クラスの場合と同様に名前解決が可能となる。

なぜテンプレート派生クラスから親クラスのメンバが解決できないか

なぜ二段階の解決をしており、テンプレートの定義時に親クラスのメンバを探すことができないかの理由については、GCCのオンラインドキュメントの13.7.2 Name Lookup, Templates, and Accessing Members of Base Classesがわかりやすい。結論から言うと、テンプレートの特殊化のため、テンプレートの定義時点ではどの関数を呼ぶべきかわからないから。

先程の例だと、

template <class T>
struct Base {
  void hoge(void){
    std::cout << "Hoge" << std::endl;
  }
};

template <class T>
struct Derived : public Base <T> {
  void hogehoge(void){
    hoge(); // (1)
  }
};

というコードで、親クラスであるBase<T>::hoge(void)を探しにいって欲しい気がする。しかし、テンプレートの特殊化により、Derivedの宣言時点ではBase<T>::hoge(void)がどのような関数になるかはわからないし、そもそも存在も保証されない。

例えば、Derivedの定義の後で

template <> struct Base <int> {};

と書かれてしまうと、テンプレートパラメタとしてintを与えて実体化したBase<int>hogeを持たない。このようにテンプレートが定義された時点では、親クラスが実際にどういう形になるかわからないため、この時点ではグローバルスコープしか見に行けない。

しかし、テンプレートが実体化される時には、全てのテンプレートパラメタの型が分かっており、かつ特殊化についての情報もあるため、通常の親クラス、派生クラスと同様の名前解決がされる。従って、

#include <iostream>

template <class T>
struct Base {
  void hoge(void){
    std::cout << "Hoge" << std::endl;
  }
};

template <class T>
struct Derived : public Base <T> {
  void hogehoge(void){
    this->hoge(); //この時点ではhoge()を探しにいかない。
  }
};

template <> struct Base <int> {};

int
main(void){
  Derived<int> d;
  d.hogehoge(); //ここではじめてhoge()の名前解決が行われ、特殊化により親クラスにhoge()が無いことがわかる。
}

と、実体化の時になってはじめて親クラスにhogeが無いことがわかるコードを書いても、

test2.cpp: In instantiation of 'void Derived<T>::hogehoge() [with T = int]':
test2.cpp:22:13:   required from here
test2.cpp:13:9: error: 'struct Derived<int>' has no member named 'hoge'
   this->hoge();
   ~~~~~~^~~~

と、「Tにintを与えて実体化しようとしたvoid Derived<T>::hogehoge()の中で参照しようとしたhogeの定義が見つからないよ」というエラーが出る。

まとめ

テンプレートの名前解決は二段階で、テンプレートパラメタに依存するシンボルの解決は実体化時、それ以外は定義時。それを知ってから、もう一度エラーメッセージを眺める。

test.cpp: In member function 'void Derived<T>::hogehoge()':
test.cpp:13:8: error: there are no arguments to 'hoge' that depend on a template parameter, so a declaration of 'hoge' must be available [-fpermissive]
   hoge();
        ^
test.cpp:13:8: note: (if you use '-fpermissive', G++ will accept your code, but allowing the use of an undeclared name is deprecated)

そしたら、「hogeにはテンプレートパラメタに依存する引数がないから、この時点でhogeの定義が見つからないといけないよ」というエラーだったとわかる。それはいいんだけど、もう少しエラーメッセージを親切にしてくれないかなぁ。

この記事によれば、clangで同様なエラーを吐く際には

note: must qualify identifier to find this declaration in dependent base class

というノートがついてたらしいんだけど、今手元のclang++で試してもこういうのは出ないみたい。

名前解決といえばADL(Argument-dependent name lookup)という特別ルールもあるっぽくて、ややこしさが倍増している。うーん。

参考にしたURL

  1. C++のちゃんとしたプログラマからは「またこのネタか」とか言われてそう。 

  2. いや、「ググってすぐにわかるようなことをわざわざQiitaの記事にするなこのキュレーション野郎が」的な意見も見かけるし、その気持もわかるんだけど、正しい検索キーワードになかなかたどり着かないプログラマ(俺とか)もいるんですよ。そういう苦労を少しでも軽減できたら・・・という趣旨です。