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();
}
ここでthis
はDerived<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
- C++11の文法と機能(C++11: Syntax and Feature) 14.5.3 依存名の名前解決
- GCCのオンラインドキュメント 13.7.2 Name Lookup, Templates, and Accessing Members of Base Classes
- Microsoft Developer Network テンプレートと名前解決
- [C++] テンプレートクラスの内部クラスの継承における基底クラスのメンバ変数検索