当前位置: 首页 > 文档资料 > C++ FAQ Lite >

[35] 模板

优质
小牛编辑
132浏览
2023-12-01

FAQs in section [35]:

  • [35.1] 模板的设计思想是什么?
  • [35.2] 什么是 “类模板”的语法/语义?
  • [35.3] 什么是“函数模板”的语法/语义?
  • [35.4] 如何确定显式调用函数模板的哪个版本?
  • [35.5] 什么是“参数化类型”?
  • [35.6] 什么是“泛型”?
  • [35.7] 当模板类型T是 int 或std::string时,我的模板函数需要进行特殊处理。对特殊类型的T我该怎么实现模板特化?
  • [35.8] 哈?你能提供一个具体的模板特化的例子吗?
  • [35.9] 但是模板函数的大部分代码是相同的,是否有办法实现模板特化并且不用重复复制所有的源代码?
  • [35.10] 所有这些模板和模板特化都会降低程序执行速度,对不对?
  • [35.11] 因此 模板重载了函数,对不对?
  • [35.12] 为什么不能分开模板的声明和定义,把定义放到.cpp文件中?
  • [35.13] 如何避免模板函数的链接错误?
  • [35.14] 如何使用C++的关键字export来避免模板链接错误?
  • [35.15] 如何避免模板类的链接错误?
  • [35.16] 为什么我收到链接错误 ,当我使用模板友元的时候?
  • [35.17] 怎么理解这些繁琐的模板错误信息?
  • [35.18]当模板派生类使用一个继承自模板基类的嵌套类型时,为什么出错?
  • [35.19]当模板派生类使用使用一个继承自模板基类的成员变量时 ,为什么出错?
  • [35.20] 前一个问题可以暗伤我?难道编译器默认地产生错误代码?

35.1 模板的设计思想是什么?

模板像是甜饼切割器,指定如何切割cookies让他们看起来大致相同(虽然Cookie由各种面团来制作,但是他们都会有相同的基本形状)。同样,类模板是描述如何建立一个类族,让所有的类看起来是基本相同;函数模板描述如何建立一个外观类似的函数族。

类模板通常用于构建类型安全的容器(although this only scratches the surface for how they can be used)。

35.2 什么是 “类模板”的语法/语义?

考虑一个容器类class Array,它的行为像一个整数数组:

 // This would go into a header file such as "__Array.h__"
 class Array {
 public:
   Array(int len=10)                  : len_(len), data_(new int[len]) { }
  ~Array()                            { delete[] data_; }
   int len() const                    { return len_;     }
   const int& operator[](int i) const { return data_[check(i)]; }  ← subscript operators often come in pairs What's the deal with "const-overloading"?")
         int& operator[](int i)       { return data_[check(i)]; }  ← subscript operators often come in pairs What's the deal with "const-overloading"?")
   Array(const Array&);
   Array& operator= (const Array&);
 private:
   int  len_;
   int* data_;
   int  check(int i) const
     { if (i < 0 || i >= len_) throw BoundsViol("Array", i, len_);
       return i; }
 };

对于浮点数数组,字符数组,std::string数组,std::string数组的数组等,反复重复上述步骤将很冗长乏味。

 // This would go into a header file such as "__Array.h__"
 template<typename T>
 class Array {
 public:
   Array(int len=10)                : len_(len), data_(new T[len]) { }
  ~Array()                          { delete[] data_; }
   int len() const                  { return len_;     }
   const T& operator[](int i) const { return data_[check(i)]; }
         T& operator[](int i)       { return data_[check(i)]; }
   Array(const Array<T>&);
   Array<T>& operator= (const Array<T>&);
 private:
   int len_;
   T*  data_;
   int check(int i) const
     { if (i < 0 || i >= len_) throw BoundsViol("Array", i, len_);
       return i; }
 };

与模板函数不同,模板类(实例化模板)在实例化时需要指明相关参数:

 int main()
 {
   Array<int>           ai;
   Array<float>         af;
   Array<char*>         ac;
   Array<std::string>   as;
   Array< Array<int> >  aai;
   ...
 }

注意最后一个例子中的两个>之间的空格符。如果没有这个空格符,编译器会看到一个>>(右移位)标记,而不是两个>

35.3 什么是“函数模板”的语法/语义?

考虑下面函数,交换两个整型参数:

 void swap(int& x, int& y)
 {
   int tmp = x;
   x = y;
   y = tmp;
 }

如果我们还要交换浮点数,长整形,字符串,集合,和文件系统等,我们就会疲于编写除了类型不同的相似的编码行。重复是电脑理想的工作,因此要用函数模板:

 template<typename T>
 void swap(T& x, T& y)
 {
   T tmp = x;
   x = y;
   y = tmp;
 }

对给定的类型每次我们使用swap()的时候,编译器将根据上述定义,并自动产生另外一个“模板函数”作为上述函数模板的实例化。例如:

 int main()
 {
   int         i,j;  /*...*/  swap(i,j);  // Instantiates a swap for int
   float       a,b;  /*...*/  swap(a,b);  // Instantiates a swap for float
   char        c,d;  /*...*/  swap(c,d);  // Instantiates a swap for char
   std::string s,t;  /*...*/  swap(s,t);  // Instantiates a swap for std::string
   ...
 }

注:“模板函数”是一个“函数模板”的实例化形态。

35.4 如何确定显式调用函数模板的哪个版本?

当你调用一个函数模板时,编译器试图推断模板类型。大部分情况下,编译器可以成功的做到这一点,但有时你可能想要帮助编译器推断出正确的类型-要么是因为它不能推断出模板类型,或者是因为它会推断出错误类型。

例如,你可能会调用一个函数模板没有模板指定的参数类型,或者你可能想让编译器在选择正确的函数模板之前,迫使它对参数做一些转换(promotions)。在这些情况下,你需要明确地告诉编译器应该调用函数的模板哪个实例化。

下面是一个示例函数模板,模板参数 T没有出现在函数的参数列表中。在这种情况下, 编译器无法推断出模板参数类型在函数被调用时。

 template<typename T>
 void f()
 {
   ...
 }

若要调用该函数把 T作为intstd::string,你可以这样做:

 #include <string>

 void sample()
 {
   f<int>();          // type T will be int in this call
   f<std::string>();  // type T will be std::string in this call
 }

这里是另一个函数,它的模板参数出现在函数的正式参数列表中(也就是说,编译器可以根据实际参数的类型推导出模板类型):

 template<typename T>
 void g(T x)
 {
   ...
 }

现在如果你想强制实行参数转换,在编译器推断模板类型之前,你可以使用上述技术。例如,如果你只是简单调用g(42),你会得到g<int>(42),但如果你想传递42给g<long>(),你可以这样做: g<long>(42)。(当然你也可以明确地转换参数,如可以g(long(42)),甚至g(42L),当然如果这样的话本例子就没有什么意义了。)

同样,如果你调用g(“xyz”),你最终会调用g<char*>(char*),但如果你想调用std::string版本g<>(),你可以这样g<std::string>(”xyz“)。(同样你也可以转换参数,例如g(std::string(“xyz”),不过那将是另一回事。)

35.5 什么是“参数化类型”?

换句话说,“类模板”。

参数化类型是一个类型,是参数化的类型或者值。 list<int>是一个被另外一个类型(int)参数化的类型( List )。

35.6 什么是“泛型”?

还是“类模板”另一种说法。

不要 与“一般性(generality)”混淆(“一般性(generality)”这只是避免过于具体的解决方案),“泛型”是指类模板。

35.7 当模板类型Tintstd::string时,我的模板函数需要进行特殊处理。对特殊类型的T我该怎么实现模板特化?

在展示如何做到这一点之前,让我们确保你不会搬起石头砸自己的脚。对于用户来说是否该函数的行为不同?换言之,是否可以观察到的行为有实质性的不同?如果是这样,你可能是在自找苦吃,你可能迷惑用户--你最好使用不同名称的函数--不要使用模板,不要使用重载。例如,如果接受int类型的代码要插入一些东西到容器并且对结果排序,但接受std::string类型的代码要从容器中删除东西并且不对结果排序,这两个函数不应该是可以重载的函数对--他们可以观察的行为是不同的,所以他们应该有不同的函数名称。

但是,如果该函数的可观察到的行为是一致的,对于所有T类型仅仅局限在各自实现细节上的不同,那么就请继续读下去。让我们看看这方面的一个例子(仅仅是概念上,不是C++代码):

 template<typename T>
 void foo(const T& x)
 {
   switch (typeof(T)) {  ← conceptual only; not C++
     case int:
       ...  ← implementation details when T is int
       break;

     case std::string:
       ...  ← implementation details when T is std::string
       break;

     default:
       ...  ← implementation details when T is neither int nor std::string
       break;
   }
 }

解决上述问题的办法就是是通过模板特化。不要使用switch语句,你需要把代码分解成单独的函数。第一个函数是默认的情况--当 Tintstd::string以外的任何其他类型时候的代码:

 template<typename T>
 void foo(const T& x)
 {
   ...  ← implementation details when T is neither int nor std::string
 }

下一步是两个特例,第一个是int特例 的代码:

 template<>
 void foo<int>(const int& x)
 {
   ...  ← implementation details when T is int
 }

接着是std::string特例 的代码:

 template<>
 void foo<std::string>(const std::string& x)
 {
   ...  ← implementation details when T is std::string
 }

好啦,大功告成!编译器将自动选择正确的特例实现根据所使用的T的类型。

35.8 哈?你能提供一个具体的模板特化的例子吗?

可以。

下面我个人使用模板特化的几种常见情况是字符串化。我通常使用模板, 将不同类型的对象字符串化,但通常需要字符串化某些特定的类型,例如当字符串化 布尔变量的时候,我喜欢用“true”与“false”来代替“1”和“0”,所以当 T 是布尔类型时,我使用std::boolalpha 。此外,我喜欢浮点输出包含所有的数字(这样我就可以看得很小的差异,等等),因此当 T是一个浮点类型时候,我使用std::setprecision。最终的结果通常如下所示:

 #include <iostream>
 #include <sstream>
 #include <iomanip>
 #include <string>
 #include <limits>

 template<typename T> inline std::string stringify(const T& x)
 {
   std::ostringstream out;
   out << x;
   return out.str();
 }

 template<> inline std::string stringify<bool>(const bool& x)
 {
   std::ostringstream out;
   out << std::boolalpha << x;
   return out.str();
 }

 template<> inline std::string stringify<double>(const double& x)
 {
   const int sigdigits = std::numeric_limits<double>::digits10;
   // or perhaps std::numeric_limits<double>::max_digits10 if that is available on your compiler
   std::ostringstream out;
   out << std::setprecision(sigdigits) << x;
   return out.str();
 }

 template<> inline std::string stringify<float>(const float& x)
 {
   const int sigdigits = std::numeric_limits<float>::digits10;
   // or perhaps std::numeric_limits<float>::max_digits10 if that is available on your compiler
   std::ostringstream out;
   out << std::setprecision(sigdigits) << x;
   return out.str();
 }

 template<> inline std::string stringify<long double>(const long double& x)
 {
   const int sigdigits = std::numeric_limits<long double>::digits10;
   // or perhaps std::numeric_limits<long_double>::max_digits10 if that is available on your compiler
   std::ostringstream out;
   out << std::setprecision(sigdigits) << x;
   return out.str();
 }

从概念上来讲他们都做同样的事情:把参数字符串化。这意味着可观察的行为是一致的,因此特化不会迷惑用户。但对于bool和浮点类型,细节的实现略有不同,因此模板特化是一个好的解决方法。

35.9 但是模板函数的大部分代码是相同的,是否有办法实现模板特化并且不用重复复制所有的源代码?

是。

例如,假设你的模板函数有很多共同的代码,与类型T相关的特定代码相对很少(仅仅是概念展示;不是C++):

 template<typename T>
 void foo(const T& x)
 {
   ... common code that works for all T types ...

   switch (typeof(T)) {  ← conceptual only; not C++
     case int:
       ... small amount of code used only when T is int ...
       break;

     case std::string:
       ... small amount of code used only when T is std::string...
       break;

     default:
       ... small amount of code used when T is neither int nor std::string ...
       break;
   }

   ... more common code that works for all T types ...
 }

如果盲目地跟从模板特化FAQ的建议,你最终将需要重复switch语句之前和之后的所有代码。两全其美的方式—既不重复相同代码又可以实现T的特定代码,是分离switch语句到一个单独的函数foo_part(),并使用模板特殊化:

 template<typename T> inline void foo_part(const T& x)
 {
   ... small amount of code used when T is neither int nor std::string ...
 }

 template<> inline void foo_part<int>(const int& x)
 {
   ... small amount of code used only when T is int ...
 }

 template<> inline void foo_part<std::string>(const std::string& x)
 {
   ... small amount of code used only when T is std::string ...
 }

主要的foo()函数是一个简单的模板-没有特化。请注意,switch语句已经被替换为foo_part()调用:

 template<typename T>
 void foo(const T& x)
 {
   ... common code that works for all T types ...

   foo_part(x);

   ... more common code that works for all T types ...
 }

正如你所看到的, foo()的函数体本身并没有任何特殊,这一切都会自动的被调用。编译器自动生成的基于 T类型 的foo(),并会生成正确的foo_part函数,根据实际编译时的X的参数类型。合适的foo_part的特化会被实例化。

35.10 所有这些模板和模板特化都会降低程序执行速度,对不对?

错误的。

这与实现代码的质量有关,结果可能会有所不同。但是不会有任何降低。模板可能会些微影响编译速度,但一旦类型在编译时被确定,它通常会生成和非模板函数(包括内联展开等)一样快的代码。

35.11 因此模板重载了函数,对不对?

是也不是。

函数模板参与重载函数的名称解析,但规则是不同的。对于模板重载,类型需要完全匹配。如果类型不完全匹配,类型不会被转换,函数模板从可行的函数集合中被排除。这就是所谓的“SFINAE”- Subsitution Failure Is Not An Error。例如:

 #include <iostream>
 #include <typeinfo>

 template<typename T> void foo(T* x)
 { std::cout << "foo<" << typeid(T).name() << ">(T*)\n"; }

 void foo(int x)
 { std::cout << "foo(int)\n"; }

 void foo(double x)
 { std::cout << "foo(double)\n"; }

 int main()
 {
     foo(42);        // matches foo(int) exactly
     foo(42.0);      // matches foo(double) exactly
     foo("abcdef");  // matches foo<T>(T*) with T = char
     return 0;
 }

在这个例子中, 在main()函数中第一或第二次调用foo不是对foo<T>的调用,因为无论42还是42.0都没有提供给编译器的任何信息来推断 。然而第三个调用,包括foo<T>并且T = char,因此它会调用foo<T>

35.12 为什么不能分开模板的声明和定义,把定义放到.cpp文件中?

如果你想知道的是只是如何解决这种情况,请阅读下面得两个s。但是,为了理解要那样,首先接受这些事实:

  1. 模板是不是一个类或函数。 模板是一个“模式”,编译器用来生成的相似的类或者函数。
  2. 为了让编译器生成的代码,它必须同时看到模板的定义(不只是声明)和特定类型/任何用于“fill in”模板的类型。例如,如果你想使用一个foo<int>,编译器必须同时看到foo模板和你要调用具体的foo<int>
  3. 编译器可能不记得另外一个.cpp文件的细节,当编译其他.cpp文件的时候。它可以 ,但大多数都没有,如果你正在阅读本FAQ,它几乎肯定不会。顺便说一句,这就是所谓的“独立编译模型”。

现在,基于这些事实,下面是一个范例,它表明为什么是这个样子。假设你有一个这样的模板Foo声明:

 template<typename T>
 class Foo {
 public:
   Foo();
   void someMethod(T x);
 private:
   T x;
 };

类似地,模板成员函数的定义:

 template<typename T>
 Foo<T>::Foo()
 {
   ...
 }

 template<typename T>
 void Foo<T>::someMethod(T x)
 {
   ...
 }

现在,假设在文件Bar.cpp的一些代码要使用foo<int>

 // Bar.cpp

 void blah_blah_blah()
 {
   ...
   Foo<int> f;
   f.someMethod(5);
   ...
 }

显然,某人某地将不得不调用“模式”的构造函数,和someMethod()函数以及做Tint的实例化。但是,如果你把构造函数和someMethod()的定义放到文件Foo.cpp,当编译Foo.cpp时,编译器将看到模板代码;当编译Bar.cpp时,编译器将看到foo<int>。但任何时候决不会同时看到模板代码和foo<int>。因此,通过上面的2号规则,它根本不会产生foo <int>::someMethod()的代码。

写给专家们的话:很明显我对以上内容作了简化。这是有意为之,所以请不要大声抱怨。如果你知道.cpp文件和编译单元的差别,类模板和模板类的差别,模板其实不只是美化的宏等,请不要抱怨:这个问题/解答不是为你而设。我简化它是为了新手能够“理解它”,即使这样可能会冒犯一些专家。

提醒:欲知解决方案,请阅读下面得两个 FAQs。

35.13 如何避免模板函数的链接错误?

当编译模板函数的.cpp文件的时候告诉C++编译器应该使用哪个实例。

例如,考虑foo.h头文件包含以下模板函数声明:

 // File "foo.h"
 template<typename T>
 extern void foo();

现在假设文件foo.cpp实际上定义的模板函数:

 // File "foo.cpp"
 #include <iostream>
 #include "foo.h"

 template<typename T>
 void foo()
 {
   std::cout << "Here I am!\n";
 }

假设文件main.cpp中使用这个模板函数通过调用foo<int>()

 // File "main.cpp"
 #include "foo.h"

 int main()
 {
   foo<int>();
   ...
 }

如果你编译和(试图)链接这两个.cpp文件,大多数编译器将生成链接错误。有三种的解决方案。第一个解决方案是物理上在.h文件中定义,即使它不是一个内联函数。这种解决办法可能(或可能不会!)造成重大代码膨胀,意味着可执行文件的大小可能会显显著增加(或者,如果你的编译器足够聪明,可能不会这么做)。

另一个解决办法是保留定义在.cpp文件中,只添加行template void foo<int>().cpp文件:

 // File "foo.cpp"
 #include <iostream>
 #include "foo.h"

 template<typename T> void foo()
 {
   std::cout << "Here I am!\n";
 }

 template void foo<int>();

如果你不能修改foo.cpp,只需创建一个新的.cpp文件,例如foo-impl.cpp如下:

 // File "foo-impl.cpp"
 #include "foo.cpp"

 template void foo<int>();

请注意, foo-impl.cpp文件包含.cpp文件,而不是.h文件。如果你觉着这样很乱,跳个踢踏舞,想想堪萨斯,跟着我重复,“我要这么做即使它很混乱。” 你需要信任我。如果不信任或者致使好奇,前面的FAQ给出了理由。

35.14 如何使用C++的关键字export来避免模板链接错误?

C++关键字export是设计用来消除包含一个模板定义(无论是在头文件中或通过实现文件中)的需要。但是,在写这篇文章时,支持此功能的唯一的知名编译器,是Comeau C++export关键字未来还是个未知数。说句公道话,一些编译器厂商表示他们可能永远不会实现它,而C++标准委员会已决定大家自己定夺。

在不支持关键字export的编译器上,如果你希望你的代码可以通过编译,并且还希望能够有效利用支持export关键字的编译器。你可以这样定义模板头文件:

 // File Foo.h

 template<typename T>
 class Foo {
   ...
 };

 #ifndef USE_EXPORT_KEYWORD
   #include "Foo.cpp"
 #endif

并定义非内联函数的源代码文件如下:

 // File Foo.cpp

 #ifndef USE_EXPORT_KEYWORD
   #define export /*nothing*/
 #endif

 export template<typename T> ...

然后,如果/当你的编译器支持export关键字的时候,并且因为某些原因你想利用该功能,只要定义符号USE_EXPORT_KEYWORD即可。

要诀就是,你现在可以开发程序, 好像你的编译器已经实现了export关键字。如果/当你的编译器真正支持该关键字的时候,只需要定义USE_EXPORT_KEYWORD标志,重新编译,马上你就可以利用该功能。

35.15 如何避免模板类的链接错误?

当编译模板类的.cpp文件得手告诉你的C++编译器应该使用哪个模板实例。(如果你已经阅读以前的问题,答案是完全一样的,所以你也许可以跳过此答案。)

作为一个例子,考虑Foo.h头文件包含以下模板类。请注意, Foo<T>::f()方法是内联的,而Foo<T>::g()Foo<T>::h()却不是。

 // File "Foo.h"
 template<typename T>
 class Foo {
 public:
   void f();
   void g();
   void h();
 };

 template<typename T>
 inline
 void Foo<T>::f()
 {
   ...
 }

现在,假设文件Foo.cpp实际定义了非内联的Foo<T>::g()Foo<T>::h()

 // File "Foo.cpp"
 #include <iostream>
 #include "Foo.h"

 template<typename T>
 void Foo<T>::g()
 {
   std::cout << "Foo<T>::g()\n";
 }

 template<typename T>
 void Foo<T>::h()
 {
   std::cout << "Foo<T>::h()\n";
 }

假设文件main.cpp使用该模板创建一个Foo<int>并调用其方法:

 // File "main.cpp"
 #include "Foo.h"

 int main()
 {
   Foo<int> x;
   x.f();
   x.g();
   x.h();
   ...
 }

如果你编译和(试图)链接这两个.cpp文件,大多数编译器将生成链接错误。有三种的解决方案。第一个解决方案是物理上在.h文件中定义,即使它不是一个内联函数。这种解决办法可能(或可能不会!)造成重大代码膨胀,意味着可执行文件的大小可能会显显著增加(或者,如果你的编译器足够聪明,可能不会这么做)。

另一个解决办法是保留定义在.cpp文件中,只添加行template class Foo<int>;.cpp文件:

 // File "Foo.cpp"
 #include <iostream>
 #include "Foo.h"

 ...definition of Foo<T>::f() is unchanged -- see above...
 ...definition of Foo<T>::g() is unchanged -- see above...

 template class Foo<int>;

如果你不能修改foo.cpp,只需创建一个新的.cpp文件,例如foo-impl.cpp如下:

 // File "Foo-impl.cpp"
 #include "Foo.cpp"

 template class Foo<int>;

请注意, foo-impl.cpp文件包含.cpp文件,而不是.h文件。如果你觉着这样很乱,跳个踢踏舞,想想堪萨斯,跟着我重复,“我要这么做即使它很混乱。” 你需要信任我。如果不信任或者致使好奇,前面的FAQ给出了理由。

如果你使用Comeau C++,你可能使用export关键字实现类似功能。

35.16 为什么我收到链接错误 ,当我使用模板友元的时候?

由于模板友类的复杂性。下面是一个常见的例子:

 #include <iostream>

 template<typename T>
 class Foo {
 public:
   Foo(const T& value = T());
   friend Foo<T> operator+ (const Foo<T>& lhs, const Foo<T>& rhs);
   friend std::ostream& operator<< (std::ostream& o, const Foo<T>& x);
 private:
   T value_;
 };

当然在某个地方我们会用到模板:

 int main()
 {
   Foo<int> lhs(1);
   Foo<int> rhs(2);
   Foo<int> result = lhs + rhs;
   std::cout << result;
   ...
 }

当然,在某个地方需要定义各成员和友元函数:

 template<typename T>
 Foo<T>::Foo(const T& value = T())
   : value_(value)
 { }

 template<typename T>
 Foo<T> operator+ (const Foo<T>& lhs, const Foo<T>& rhs)
 { return Foo<T>(lhs.value_ + rhs.value_); }

 template<typename T>
 std::ostream& operator<< (std::ostream& o, const Foo<T>& x)
 { return o << x.value_; }

一个潜在问题是编译器如何理解类声明中的friends行。在看到friends行的时候,它还不知道友元函数本身也是模板,它假定他们不是模板函数,就像下面这样:

 Foo<int> operator+ (const Foo<int>& lhs, const Foo<int>& rhs)
 { ... }

 std::ostream& operator<< (std::ostream& o, const Foo<int>& x)
 { ... }

当你调用运算符+或运算符<<的时候,这种假设导致编译器生成一个对非模板函数的调用,但是链接器会给你一个“未定义的外部函数”错误,因为你从来没有真正的定义这些非模板函数。

解决的办法是在编译器编译类体的时候,让编译器知道运算符+和运算符<<本身是模板。有几种方法可以做到这一点;一个简单的方法是在定义函数模板类 Foo的时候预先声明模板友元:

 template<typename T> class Foo;  // pre-declare the template class itself
 template<typename T> Foo<T> operator+ (const Foo<T>& lhs, const Foo<T>& rhs);
 template<typename T> std::ostream& operator<< (std::ostream& o, const Foo<T>& x);

frend行中你也需要加入<>,如下所示:

 #include <iostream>

 template<typename T>
 class Foo {
 public:
   Foo(const T& value = T());
   friend Foo<T> operator+ <> (const Foo<T>& lhs, const Foo<T>& rhs);
   friend std::ostream& operator<< <> (std::ostream& o, const Foo<T>& x);
 private:
   T value_;
 };

这些写法将有助于编译器更好地了解友元函数。值得一提的是,它会发现友元函数本身是模板。这消除了混乱。

另一种方法是在类中同时声明和定义该友元函数。例如:

 #include <iostream>

 template<typename T>
 class Foo {
 public:
   Foo(const T& value = T());

   friend Foo<T> operator+ (const Foo<T>& lhs, const Foo<T>& rhs)
   {
        ...
   }

   friend std::ostream& operator<< (std::ostream& o, const Foo<T>& x)
   {
        ...
   }

 private:
   T value_;
 };

35.17 怎么理解这些繁杂的模板错误信息?

这里有一个免费工具, 可以转换错误信息便于理解。在撰写本文的时候,它工作用于下列编译器:Comeau C +,Intel C++,CodeWarrior C++,gcc,Borland C++,Microsoft Visual C++和EDG C++。

这里有一个例子,下面是一些原始的gcc的错误信息:

 rtmap.cpp: In function int main()':
 rtmap.cpp:19: invalid conversion from int' to 
    std::_Rb_tree_node<std::pair<const int, double> >*'
 rtmap.cpp:19:   initializing argument 1 of std::_Rb_tree_iterator<_Val, _Ref,
    _Ptr>::_Rb_tree_iterator(std::_Rb_tree_node<_Val>*) [with _Val =
    std::pair<const int, double>, _Ref = std::pair<const int, double>&, _Ptr =
    std::pair<const int, double>*]'
 rtmap.cpp:20: invalid conversion from int' to 
    std::_Rb_tree_node<std::pair<const int, double> >*'
 rtmap.cpp:20:   initializing argument 1 of std::_Rb_tree_iterator<_Val, _Ref,
    _Ptr>::_Rb_tree_iterator(std::_Rb_tree_node<_Val>*) [with _Val =
    std::pair<const int, double>, _Ref = std::pair<const int, double>&, _Ptr =
    std::pair<const int, double>*]'
 E:/GCC3/include/c++/3.2/bits/stl_tree.h: In member function void
    std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::insert_unique(_II,
     _II) [with _InputIterator = int, _Key = int, _Val = std::pair<const int,
    double>, _KeyOfValue = std::_Select1st<std::pair<const int, double> >,
    _Compare = std::less<int>, _Alloc = std::allocator<std::pair<const int,
    double> >]':
 E:/GCC3/include/c++/3.2/bits/stl_map.h:272:   instantiated from void std::map<_
 Key, _Tp, _Compare, _Alloc>::insert(_InputIterator, _InputIterator) [with _Input
 Iterator = int, _Key = int, _Tp = double, _Compare = std::less<int>, _Alloc = st
 d::allocator<std::pair<const int, double> >]'
 rtmap.cpp:21:   instantiated from here
 E:/GCC3/include/c++/3.2/bits/stl_tree.h:1161: invalid type argument of unary *
    '

以下是经过过滤的错误信息(注:你可以配置工具让它显示更多的信息,下面输出的设置是剪裁信息到最少):

 rtmap.cpp: In function int main()':
 rtmap.cpp:19: invalid conversion from int' to iter'
 rtmap.cpp:19:   initializing argument 1 of iter(iter)'
 rtmap.cpp:20: invalid conversion from int' to iter'
 rtmap.cpp:20:   initializing argument 1 of iter(iter)'
 stl_tree.h: In member function void map<int,double>::insert_unique(_II, _II)':
     [STL Decryptor: Suppressed 1 more STL standard header message]
 rtmap.cpp:21:   instantiated from here
 stl_tree.h:1161: invalid type argument of unary *'

以下是上面例子的源代码:

 #include <map>
 #include <algorithm>
 #include <cmath>

 const int values[] = { 1,2,3,4,5 };
 const int NVALS = sizeof values / sizeof (int);

 int main()
 {
     using namespace std;

     typedef map<int, double> valmap;

     valmap m;

     for (int i = 0; i < NVALS; i++)
         m.insert(make_pair(values[i], pow(values[i], .5)));

     valmap::iterator it = 100;              // error
     valmap::iterator it2(100);              // error
     m.insert(1,2);                          // error

     return 0;
 }

35.18 当模板派生类使用一个继承自模板基类的嵌套类型时,为什么出错?

你也许很吃惊,下面的代码是无效的C++代码,即使如此通过有些编译器:

 template<typename T>
 class B {
 public:
   class Xyz { ... };  ← type nested in class B<T>
   typedef int Pqr;    ← type nested in class B<T>
 };

 template<typename T>
 class D : public B<T> {
 public:
   void g()
   {
     Xyz x;  ← bad (even though some compilers erroneously (temporarily?) accept it)
     Pqr y;  ← bad (even though some compilers erroneously (temporarily?) accept it)
   }
 };

这可能会让你很伤脑筋,最好坐下来听我讲。

在函数D<T>::g()内,名字xyzPqr不依赖于模板参数T,所以他们被称作为nondependent名字。另一方面B<T> 依赖模板参数T,因此 B<T> 称作dependent名字

规则是这样的:当查找nondependent名字(比如XyzPqr)的时候,编译器不会查找dependent基类(如B <T>中 )。因此,编译器不知道他们甚至还存在,更不用说知道它们也是类型。

这时,程序员有时会添加前缀B <T>::,例如:

 template<typename T>
 class D : public B<T> {
 public:
   void g()
   {
     B<T>::Xyz x;  ← bad (even though some compilers erroneously (temporarily?) accept it)
     B<T>::Pqr y;  ← bad (even though some compilers erroneously (temporarily?) accept it)
   }
 };

可惜这也行不通,因为这些名字(你准备好了吗?坐下来?)不一定是类型。 "哈?!?" ?"不是类型?!?" ?。“太搞了吧!任何傻瓜都可以看到他们是类型;只要看上一眼!”,你抗议。抱歉,事实是,他们可能不是类型。原因是,有可能是B<T>的特化,假设B<Foo>,其中 B <Foo>::Xyz是一个数据成员。由于这种潜在的特化,编译器不能假设B<T>::Xyz是一个类型,直到它知道T。解决方案是通过typename关键字提示编译器:

 template<typename T>
 class D : public B<T> {
 public:
   void g()
   {
     typename B<T>::Xyz x;  ← good
     typename B<T>::Pqr y;  ← good
   }
 };

35.19 当模板派生类使用使用一个继承自模板基类的成员变量时 ,为什么出错?

你也许很吃惊,下面的代码是无效的C++代码,即使如此通过有些编译器:

 template<typename T>
 class B {
 public:
   void f() { }  ← member of class B<T>
 };

 template<typename T>
 class D : public B<T> {
 public:
   void g()
   {
     f();  ← bad (even though some compilers erroneously (temporarily?) accept it)
   }
 };

这可能会让你很伤脑筋,最好坐下来听我讲。

在函数D<T>::g()内,名字f不依赖于模板参数T,所以他们被称作为nondependent名字。另一方面B<T> 依赖模板参数T,因此 B<T> 称作dependent名字

规则是这样的:当查找nondependent名字(比如f)的时候,编译器不会查找dependent基类(如B <T>中 )。

这并不意味着继承不起作用。类D <int>是仍然继承自类B <int>,编译器仍然让你可以隐式的做is- a转换(例如,D<int>*B <int> *),动态绑定仍然有效当虚函数被调用时,等等。但有一个如何查找名称的问题。

替代方案:

  • 改变的f()的调用为this->f()。由于在模板中this指针一直是隐式实现的,this->f()要依赖查找,因此推迟到模板实例化时,此时所有基类都会被查找。
  • 在调用f()之前,插入 using B<T>::f;语句。
  • 改变的f()的调用为B <T>::f()。 但是请注意,如果f()是虚函数,这可能没有给你想要的东西,因为它禁止了虚函数带调用机制。

35.20 前一个问题可以暗伤我?难道编译器默认地产生错误代码?

是。

由于non-dependent类型 and non-dependent成员不会在dependent模板在基础类中搜索,编译器将搜索封闭范围,比如封闭名字空间。这可能会导致它在你没有意识到的情况下(!)做错误的事情。

例如:

 class Xyz { ... };  ← global ("namespace scope") type
 void f() { }        ← global ("namespace scope") function

 template<typename T>
 class B {
 public:
   class Xyz { ... };  ← type nested in class B<T>
   void f() { }        ← member of class B<T>
 };

 template<typename T>
 class D : public B<T> {
 public:
   void g()
   {
     Xyz x;  ← suprise: you get the global Xyz!!
     f();    ← suprise: you get the global f!!
   }
 };

D<T>::g()内的Xyzf将被解析为全局变量,而不是继承自类B <T>,这恐怕不是你的真正意图。

别埋怨我没有警告过你。