# [35] 模板
## 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`作为`int`或`std::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 当模板类型`T`是 `int`或`std::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`语句,你需要把代码分解成单独的函数。第一个函数是默认的情况--当 `T`是`int`或`std::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()`函数以及做`T`为`int`的实例化。但是,如果你把构造函数和`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++](http://www.comeaucomputing.com/tryitout)。`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++](http://www.comeaucomputing.com/tryitout "www.comeaucomputing.com/tryitout"),你可能使用`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 怎么理解这些繁杂的模板错误信息?
这里有一个免费工具, [可以转换错误信息便于理解](http://www.bdsoft.com/tools/stlfilt.html)。在撰写本文的时候,它工作用于下列编译器: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()`内,名字`xyz`和`Pqr`不依赖于模板参数`T`,所以他们被称作为nondependent名字。另一方面`B<T>` 依赖模板参数`T`,因此 `B<T>` 称作_dependent名字_。
规则是这样的:当查找nondependent名字(比如`Xyz`和`Pqr`)的时候,编译器不会查找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()`内的`Xyz`和`f`将被解析为全局变量,而不是继承自类`B <T>`,这恐怕不是你的真正意图。
别埋怨我没有警告过你。
- C++ FAQ Lite
- [1] 复制许可
- [2] 在线站点分发本文档
- [3] C++-FAQ-Book 与 C++-FAQ-Lite
- [6] 综述
- [7] 类和对象
- [8] 引用
- [9] 内联函数
- [10] 构造函数
- [11] 析构函数
- [12] 赋值算符
- [13] 运算符重载
- [14] 友元
- [15] 通过 &lt;iostream&gt; 和 &lt;cstdio&gt;输入/输出
- [16] 自由存储(Freestore)管理
- [17] 异常和错误处理
- [18] const正确性
- [19] 继承 — 基础
- [20] 继承 — 虚函数
- [21] 继承 — 适当的继承和可置换性
- [22] 继承 — 抽象基类(ABCs)
- [23] 继承 — 你所不知道的
- [24] 继承 — 私有继承和保护继承
- [27] 编码规范
- [28] 学习OO/C++
- [31] 引用与值的语义
- [32] 如何混合C和C++编程
- [33] 成员函数指针
- [35] 模板
- [36] 序列化与反序列化
- [37] 类库