模板中的名字查找問題


問題起源

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

#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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM