模板中的名字查詢問題

twoon發表於2014-06-15

問題起源

先看下面很簡單的一小段程式。

#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++ 的標準有這樣的要求:

  1. 如果名字不依賴於模板中的模板引數,則該符號必須定義在當前模板可見的上下文內。

  2. 如果名字是依賴於模板中的模板引數,則該符號是在例項化該模板時,才對該符號進行查詢。

也就是說,對於前面提到的例子,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,因此名字的查詢就會被推遲到該類被例項化時才去基類中查詢。

兩階段名字查詢

從前面的介紹,我們可以看到編譯器對模板中引用的符號的查詢是分為兩個階段的:

  1. 符號不依賴於當前模板引數,該符號則被當作是外部的符號,直接在當前模板所在的域中去查詢。

  2. 符號如依賴於當前模板引數,則對該符號的查詢被推遲到模板被例項化時。

為什麼要這樣區別對待呢?原因其實很簡單,編譯器在看到模板 Derived 的定義時,還不能確定它的基類最後是怎樣的:Base 有可能會在後面被特化,使得最後被繼承的具體基類中不一定還有 fun() 函式。

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 中 gun() 函式對 Base::baseT 的引用會造成編譯器的迷惑,它在看到 Derided 的定義時,根本無從知道 Base::baseT 究竟是一個變數名,還是一個型別,以及什麼型別?而它又不能直接把這一部分相關的程式碼全部都推遲到第二階段再進行,因此在這兒它就會報錯了:它可以不知道這個型別最後是什麼型別,但它必須知道它究竟是不是型別,如果連這個都不知道,接下來相關的程式碼它都沒法去解析了。因此,實際上,編譯器在看到一個與模板引數相關的符號時,預設它都是當作一個變數來處理的,所以在上述的例子中,編譯器在看到 Derived 的定義時,它直接把 Base::baseT 當成了一個變數來處理,所以就會報錯了。

那麼,我們要怎樣才能讓編譯器知道其實 Base::baseT 是一個型別呢? 必須得顯式地告訴它,因此需要在引用 Base::baseT 時,顯式地加入一個關鍵字:typename.

template <typename T>
struct Derived : Base<T>
{
   void gun()
   {
      typename Base<T>::baseT p = "abc";
   }
};

此時,編譯器看到有 typename 顯式地指明 baseT 是一個型別,它就不會再把它預設當成是一個變數了,從而使得名字查詢的第一個階段可以繼續下去。

總結

模板中名字的查詢會因為該名字是否依賴於模板引數而有所不同。

依賴於模板引數的名字(如函式的引數的型別是模板的引數),其符號解析會在第二階段進行,其查詢方式有兩個:

  1. 在模板定義的域內可見的符號。(很嚴格)
  2. 在例項化模板的域內通過 ADL 的方式查詢符號。(也很嚴格,杜絕了不同 namespace 內部重複定義導致衝突的問題)。

而不依賴於模板引數的符號,則只會在定義模板的可見域內進行查詢,語言的定義嚴格如上所述,但實際編譯器的支援上,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

相關文章