問題起源
先看下面很簡單的一小段程式。
#include <iostream>
template <typename T>
struct Base
{
void fun()
{
std::cout << "Base::fun" << std::endl;
}
};
template <typename T>
struct Derived : Base<T>
{
void gun()
{
std::cout << "Derived::gun" << std::endl;
fun();
}
};
這段程式碼在 GCC 下很意外地編譯不過,原因竟然是找不到 fun 的定義,可是明明就定義在基類中了好嗎!為什麼視而不見呢?顯然這和編譯器對名字的查詢方式有關,那這裡面究竟有什麼玄機呢?上述程式碼是寫得不規範,還是 GCC 竟然存在這樣愚蠢而又莫名其妙的 bug?
C++ 標準的要求
對於模板中引用的符號,C++ 的標準有這樣的要求:
如果名字不依賴於模板中的模板引數,則該符號必須定義在當前模板可見的上下文內。
如果名字是依賴於模板中的模板引數,則該符號是在例項化該模板時,才對該符號進行查詢。
也就是說,對於前面提到的例子,gun() 函式中呼叫 fun(),由於該 fun() 並不依賴於 Derived 的模板引數T,因此在編譯器看來該呼叫就相當於 ::fun(),直接把它當成是一個外部的符號去查詢,而此時外部又沒有定義該函式,因此就報錯了。要去除這種錯誤,解決的方法很簡單,只要在呼叫 fun 的地方,人為地加上該呼叫對模板引數的依賴則可。
template <typename T>
struct Derived : Base<T>
{
void gun()
{
std::cout << "Derived::gun" << std::endl;
this->fun();// or Base<T>::fun();
}
};
加上 this 之後,fun 就依賴於當前 Derived 類,也就間接依賴了模板引數T,因此名字的查詢就會被推遲到該類被例項化時才去基類中查詢。
兩階段名字查詢
從前面的介紹,我們可以看到編譯器對模板中引用的符號的查詢是分為兩個階段的:
符號不依賴於當前模板引數,該符號則被當作是外部的符號,直接在當前模板所在的域中去查詢。
符號如依賴於當前模板引數,則對該符號的查詢被推遲到模板被例項化時。
為什麼要這樣區別對待呢?原因其實很簡單,編譯器在看到模板 Derived 的定義時,還不能確定它的基類最後是怎樣的:Base
template <>
struct Base<int>
{
void fun2()
{
std::cout << "Specialized, Base::fun2" << std::endl;
}
};
因此編譯器在看到模板類的定義時,還不能判斷它的基類最後會被例項化成怎樣,所以對依賴於模板引數的符號的查詢只能推遲到該模板被例項化時才進行,而如果符號不依賴於模板引數,顯然沒有這個限制,因此可以在看到模板的定義時就直接進行查詢,於是就出現了對不同符號的兩階段查詢。
符號與型別問題
對於前面介紹中提到的符號,我們其實預設指的是變數,細心的讀者可能會想到,在繼承類中引用的符號,還可能會是型別,而由於模板特化的存在,在名字查詢的第一階段編譯器也是沒法判斷出該符號最後到底是怎樣的型別,甚至不能知道是不是一個型別。
template <typename T>
struct Base
{
typedef char* baseT;
};
template <typename T>
struct Derived : Base<T>
{
void gun()
{
Base<T>::baseT p = "abc";
}
};
template <>
struct Base<int>
{
typedef int baseT;
};
template <>
struct Base<float>
{
int baseT;
};
如上例子,Derived
那麼,我們要怎樣才能讓編譯器知道其實 Base
template <typename T>
struct Derived : Base<T>
{
void gun()
{
typename Base<T>::baseT p = "abc";
}
};
此時,編譯器看到有 typename 顯式地指明 baseT 是一個型別,它就不會再把它預設當成是一個變數了,從而使得名字查詢的第一個階段可以繼續下去。
總結
模板中名字的查詢會因為該名字是否依賴於模板引數而有所不同。
依賴於模板引數的名字(如函式的引數的型別是模板的引數),其符號解析會在第二階段進行,其查詢方式有兩個:
而不依賴於模板引數的符號,則只會在定義模板的可見域內進行查詢,語言的定義嚴格如上所述,但實際編譯器的支援上,msvc 不支援兩階段的查詢(vc 2010 以前),gcc 的實現在 4.7 以前也不完全符合標準,一個比較全面的符合規範的例子,請參看如下:
void f(char); // 第一個 f 函式
template<class T>
void g(T t) {
f(1); // 不依賴引數的符號,符號解釋在第一階段進行,找到 ::f(char)
f(T(1)); // 依賴引數的符號: 查詢推遲
f(t); // 依賴引數的符號: 查詢推遲
}
enum E { e };
void f(E); // 第二個 f 函式
void f(int); // 第三個 f 函式
void h() {
g(32); // 例項化 g<int>, 此時進行查詢 f(T(1)) 和 f(t)
// f(t) 的查詢找到 f(char),因為是通過非 ADL 方式查詢的(T 是 int,ADL 失效),而定義模板的域內只有 f(char)。
// 同理,f(T(1)) 的查詢也只找到 f(char)。
g(e); // 例項化 g<E>, 此時進行查詢 f(T(1)) 和 f(t),因為引數都是使用者定義的型別,ADL 起效,因此兩者均找到了 f(E),
}
typedef double A;
template<class T> class B {
typedef int A;
};
template<class T> struct X : B<T> {
A a; // 此處 A 為 double
};
【引用】
http://gcc.gnu.org/onlinedocs/gcc/Name-lookup.html
http://womble.decadent.org.uk/c++/template-faq.html
http://en.cppreference.com/w/cpp/language/unqualified_lookup
https://gcc.gnu.org/gcc-4.7/porting_to.html
http://en.cppreference.com/w/cpp/language/adl