模板
模板背后的理念是什么?
模板就像一个饼干模具,它规定了如何切割出大致相同的饼干(尽管饼干可以用各种面团制作,但它们都将具有相同的基本形状)。同理,类模板是用于描述如何构建一系列基本相同的类的模具,而函数模板则描述了如何构建一系列相似的函数。
类模板常用于构建类型安全的容器(尽管这只是其用途的冰山一角)。
“类模板”的语法/语义是什么?
考虑一个行为类似于整数数组的容器 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
int& operator[](int i) { return data_[check(i)]; } // Subscript operators often come in pairs
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;
}
};
一遍又一遍地为 float
、char
、std::string
、Array-of-std::string
等类型的 Array 重复上述操作会变得很繁琐。相反,您在类定义之前添加 template<typename T>
(T
可以是您想要的任何标识符,T
只是最常用的一个,尤其是在示例中)。然后,在引用数据类型的地方,不再使用 int
或 float
或 char
,而是使用 T
。此外,当引用模板时,不再仅仅将类称为 Array,而是 Array<T>
;当引用特定实例化时,则是 Array<int>
、Array<float>
等。
// 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(Array<T>&&);
Array<T>& operator= (const Array<T>&);
Array<T>& operator= (Array<T>&&);
private:
int len_;
T* data_;
int check(int i) const {
assert(i >= 0 && i < len_);
return i;
}
};
就像普通类一样,您可以选择在类外部定义您的方法
template<typename T>
class Array {
public:
int len() const;
// ...
};
template<typename T>
inline // See below if you want to make this non-inline
int Array<T>::len() const
{
// ...
}
与函数模板不同,类模板(类模板的实例化)需要明确其实例化所依据的参数
int main()
{
Array<int> ai;
Array<float> af;
Array<char*> ac;
Array<std::string> as;
Array<Array<int>> aai;
// ...
}
请注意,在 C++11 之前,最后一个示例中的两个 >
之间需要一个空格。如果没有这个空格,C++98/C++03 编译器会将 >>
视为右移操作符标记,而不是两个 >
。您真幸运,C++11 中不再是这种情况了!
“函数模板”的语法/语义是什么?
考虑这个交换两个整数参数的函数
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
// ...
}
注意:“模板函数”是“函数模板”的实例化。
有时,您确实希望明确使用的类型。
如何明确选择应调用哪个版本的函数模板?
当您调用函数模板时,编译器会尝试推断模板类型。大多数情况下,它可以成功推断,但偶尔您可能需要帮助编译器推断出正确的类型——要么是因为它根本无法推断出类型,要么是因为它可能会推断出错误的类型。
例如,您可能正在调用一个函数模板,该函数模板的参数列表中不包含其模板参数类型,或者您可能希望强制编译器在选择正确的函数模板之前对参数进行某些提升。在这些情况下,您需要明确告诉编译器应该调用哪个函数模板实例化。
这是一个示例函数模板,其中模板参数 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*)
,但如果您想调用 g<>()
的 std::string
版本,您可以说 g<std::string>("xyz")
。(同样,您也可以提升参数,例如 g(std::string("xyz"))
,但这又是另一回事了。)
另一种必须指定类型的情况是,函数接受两个相同类型的参数,但您给它两个不同的类型。
template<typename T>
void g(T x, T y);
int m = 0;
long n = 1;
g(m, n);
由于 m
和 n
具有不同的类型,编译器无法推断 T
应该使用哪种类型,因此您必须告诉它使用哪种类型
template<typename T>
void g(T x, T y);
int m = 0;
long n = 1;
g<int>(m, n);
什么是“参数化类型”?
另一种说法是:“类模板”。
参数化类型是根据另一个类型或某个值进行参数化的类型。List<int>
是一种类型 (List
),它根据另一个类型 (int
) 进行参数化。
什么是“泛型”?
另一种说法是:“类模板”。
不要与“通用性”(general_ity,仅指避免过于具体的解决方案)混淆,“泛型”(genericity)指的是类模板。
当模板类型 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
语句。第一个函数是 default
情况——当 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
时。
嗯?你能提供一个不使用 foo
和 bar
的模板特化示例吗?
是的。
我个人使用模板特化的一种方法是字符串化。我通常使用模板来字符串化各种类型的各种对象,但我经常需要对字符串化某些特定类型的代码进行特化。例如,当字符串化 bool
时,我更喜欢 "true"
和 "false"
而不是 "1"
和 "0"
,所以当 T
是 bool
时,我使用 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
和浮点类型略有不同,因此模板特化是一个很好的方法。
但是我的模板函数中的大部分代码都是相同的;有没有办法在不重复所有源代码的情况下获得模板特化的好处?
是的。
例如,假设您的模板函数有一堆通用代码和相对少量的 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 ...
}
如果您盲目地遵循关于模板特化的常见问题解答中的建议,您最终会重复所有伪开关语句前后的代码。获得两全其美的最佳方法——在不重复整个函数的情况下获得 T
特定部分的益处——是将伪开关语句部分提取到一个单独的函数 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()
将是一个简单的模板——没有特化。请注意,伪开关语句已被对 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
。所有这些都将自动解决。编译器根据类型 T
为您生成 foo
,并将根据 x
参数的实际编译时已知类型生成正确类型的 foo_part
函数。 foo_part
的适当特化将被实例化。
所有这些模板和模板特化肯定会拖慢我的程序,对吗?
错了。
这是一个实现质量问题,因此您的结果可能会有所不同。然而,通常根本不会有任何减速。如果有什么不同的话,模板可能会稍微影响编译速度,但一旦类型在编译时由编译器解析,它通常会生成与非模板函数一样快的代码,包括内联展开适当的函数等。
所以模板就是重载,对吗?
是,也不是。
函数模板参与重载函数的名称解析,但规则不同。要使模板在重载解析中被考虑,类型必须完全匹配。如果类型不完全匹配,则不考虑转换,模板将简单地从可行函数集中删除。这就是所谓的“SFINAE”——替换失败不是错误。示例
#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;
}
在这个例子中,foo<T>
不能被 main
函数体中对 foo
的第一次或第二次调用所考虑,因为 42 和 42.0 都不能为编译器提供任何信息来推断 T
。然而,第三次调用包含了 foo<T>
,其中 T
= char
,并且它获胜。
为什么我不能将模板类的定义与其声明分开,并将其放入 .cpp 文件中?
如果您只想知道如何解决这种情况,请阅读接下来的两个常见问题。但为了理解为什么会这样,首先接受这些事实
- 模板不是类也不是函数。模板是一个“模式”,编译器使用它来生成一系列类或函数。
- 为了让编译器生成代码,它必须同时看到模板定义(而不仅仅是声明)以及用于“填充”模板的特定类型/其他信息。例如,如果您尝试使用
Foo<int>
,编译器必须同时看到Foo
模板以及您正在尝试创建一个特定的Foo<int>
的事实。 - 您的编译器在编译一个
.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
文件和编译单元的区别,类模板和模板类的区别,以及模板实际上不只是被美化的宏的事实,那么请不要抱怨:这个特定的问题/答案一开始就不是针对您的。我简化了内容,以便新手能够“理解”,即使这样做可能会冒犯一些专家。
提醒: 阅读接下来的两个常见问题解答,以获取此问题的一些解决方案。
如何避免我的模板函数出现链接器错误?
此答案将因 C++11 extern template
而更新。请关注此空间,近期将有更新!!
在编译模板函数的 .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 文件中,即使它不是 inline
函数。这种解决方案可能会(也可能不会!)导致显著的代码膨胀,这意味着您的可执行文件大小可能会急剧增加(或者,如果您的编译器足够智能,则可能不会;尝试一下看看)。
另一种解决方案是将模板函数的定义保留在 .cpp 文件中,只需在该文件中添加一行 template void foo<int>();
即可
// 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
#include
的是一个 .cpp 文件,而不是 .h 文件。如果这让您感到困惑,请敲击您的脚跟两次,想想堪萨斯州,然后跟着我说:“我无论如何都会这样做,即使它令人困惑。”您可以在这一点上相信我。但是,如果您不信任我,或者只是好奇,原因前面已经给出。
C++ 关键字 export
如何帮助解决模板链接器错误?
此答案将因 C++11 extern template
而更新。请关注此空间,近期将有更新!!
C++ 关键字 export
最初是为了消除包含模板定义(通过在头文件中提供定义或包含实现文件)的需要而设计的。然而,只有少数编译器支持此功能,例如 Comeau C++ 和 Sun Studio,普遍的共识是这不值得麻烦。
因此,在 C++11 标准中,export
功能已从语言中移除。它仍然是一个保留字,但不再具有任何含义。
如果您正在使用的编译器支持 export
关键字,它可能会继续通过某种编译器选项或扩展来支持该关键字,直到其用户不再使用它。如果您已经有使用 export
的代码,您可以使用一种相当简单的纪律,以便在您的编译器完全停止支持它时,您的代码能够轻松迁移。只需像这样定义您的模板头文件
// File Foo.h
#ifdef USE_EXPORT_KEYWORD
export
#endif
template<typename T>
class Foo {
// ...
};
#ifndef USE_EXPORT_KEYWORD
#include "Foo.cpp"
#endif
并在源文件中像这样定义非内联函数
// File Foo.cpp
#ifdef USE_EXPORT_KEYWORD
export
#endif
template<typename T> ...
然后使用 -DUSE_EXPORT_KEYWORD
编译,或者任何等效的编译器选项允许您设置像 USE_COMPILER_KEYWORD
这样的预处理器符号,如果您的编译器移除对 export
的支持,只需移除该编译器选项即可。
如何避免我的模板类出现链接器错误?
此答案将因 C++11 extern template
而更新。请关注此空间,近期将有更新!!
在编译模板类的 .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
实际定义了非 inline
方法 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 文件中,即使它们不是 inline
函数。这种解决方案可能会(也可能不会!)导致显著的代码膨胀,这意味着您的可执行文件大小可能会急剧增加(或者,如果您的编译器足够智能,则可能不会;尝试一下看看)。
另一种解决方案是将模板函数的定义保留在 .cpp 文件中,只需在该文件中添加一行 template class Foo<int>;
即可
// 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
#include
的是一个 .cpp 文件,而不是 .h 文件。如果这让您感到困惑,请敲击您的脚跟两次,想想堪萨斯州,然后跟着我说:“我无论如何都会这样做,即使它令人困惑。”您可以在这一点上相信我。但是,如果您不信任我,或者只是好奇,原因前面已经给出。
如果您正在使用 Comeau C++,您可能想了解 export
关键字。
为什么我使用模板友元时会出现链接器错误?
啊,模板友元的复杂性。这是一个人们通常想要做的例子
#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_; }
问题发生在编译器在类定义本身中看到 friend
行时。那一刻,它还不知道 friend
函数本身就是模板;它假设它们是非模板的,像这样
Foo<int> operator+ (const Foo<int>& lhs, const Foo<int>& rhs)
{ /*...*/ }
std::ostream& operator<< (std::ostream& o, const Foo<int>& x)
{ /*...*/ }
当您调用 operator+
或 operator<<
函数时,此假设会导致编译器生成对非模板函数的调用,但链接器会给您一个“未定义外部”错误,因为您从未实际定义那些非模板函数。
解决方案是让编译器在检查类主体时相信 operator+
和 operator<<
函数本身就是模板。有几种方法可以做到这一点;一种简单的方法是在模板类 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);
您还在 friend
行中添加了 <>
,如下所示
#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_;
};
在编译器看到这些神奇的东西之后,它会对 friend
函数有更深入的了解。特别是,它会意识到 friend
行指的是本身就是模板的函数。这消除了混淆。
另一种方法是在声明友元函数的同时,在类主体内部定义友元函数。例如:
#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_;
};
为什么我不能为我的模板参数定义约束?
(注意:此常见问题解答有些过时,需要针对 static_assert
进行更新。)
嗯,你可以,而且它相当容易和通用。
考虑
template<class Container>
void draw_all(Container& c)
{
for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}
如果存在类型错误,它将出现在相当复杂的 for_each()
调用的解析中。例如,如果容器的元素类型是 int
,那么我们就会得到某种与 for_each()
调用相关的模糊错误(因为我们不能为 int
调用 Shape::draw()
)。
为了尽早捕获此类错误,您可以编写
template<class Container>
void draw_all(Container& c)
{
Shape* p = c.front(); // accept only containers of Shape*s
for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}
伪变量 p
的初始化将触发大多数当前编译器发出可理解的错误消息。这种技巧在所有语言中都很常见,并且必须针对所有新颖的构造进行开发。在生产代码中,您可能会这样写:
template<class Container>
void draw_all(Container& c)
{
typedef typename Container::value_type T;
Can_copy<T,Shape*>(); // accept containers of only Shape*s
for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}
这清楚地表明你正在进行断言。Can_copy
模板可以这样定义:
template<class T1, class T2> struct Can_copy {
static void constraints(T1 a, T2 b) { T2 c = a; b = a; }
Can_copy() { void(*p)(T1,T2) = constraints; }
};
Can_copy
检查(在编译时)T1
是否可以赋值给 T2
。Can_copy<T,Shape*>
检查 T
是否为 Shape*
或指向从 Shape
公开派生的类的指针,或具有用户定义的到 Shape*
转换的类型。请注意,定义接近最小化
- 一行用于命名要检查的约束以及要检查它们的类型
- 一行列出检查的具体约束(
constraints()
函数) - 提供一种触发检查的方法(构造函数)
另请注意,该定义具有以下理想属性
- 您可以表达约束,而无需声明或复制变量,因此约束的编写者不必对类型如何初始化、对象是否可以复制、销毁等做出假设(当然,除非这些是约束要测试的属性)
- 当前编译器不会为使用约束生成代码
- 定义或使用约束不需要宏
当前编译器为失败的约束提供可接受的错误消息,包括“constraints”(给读者一个提示)、约束的名称以及导致失败的具体错误(例如“cannot initialize Shape*
by double*
”)
那么为什么像 Can_copy()
—— 或者更优雅的东西 —— 没有出现在语言中呢?一种直接指定这些约束的方法正在我们讨论时进行中——请参见 Concepts Lite。
在此之前,上述思想非常普遍。毕竟,当我们编写模板时,我们拥有 C++ 的全部表达能力。考虑
template<class T, class B> struct Derived_from {
static void constraints(T* p) { B* pb = p; }
Derived_from() { void(*p)(T*) = constraints; }
};
template<class T1, class T2> struct Can_copy {
static void constraints(T1 a, T2 b) { T2 c = a; b = a; }
Can_copy() { void(*p)(T1,T2) = constraints; }
};
template<class T1, class T2 = T1> struct Can_compare {
static void constraints(T1 a, T2 b) { a==b; a!=b; a<b; }
Can_compare() { void(*p)(T1,T2) = constraints; }
};
template<class T1, class T2, class T3 = T1> struct Can_multiply {
static void constraints(T1 a, T2 b, T3 c) { c = a*b; }
Can_multiply() { void(*p)(T1,T2,T3) = constraints; }
};
struct B { };
struct D : B { };
struct DD : D { };
struct X { };
int main()
{
Derived_from<D,B>();
Derived_from<DD,B>();
Derived_from<X,B>();
Derived_from<int,B>();
Derived_from<X,int>();
Can_compare<int,float>();
Can_compare<X,B>();
Can_multiply<int,float>();
Can_multiply<int,float,double>();
Can_multiply<B,X>();
Can_copy<D*,B*>();
Can_copy<D,B*>();
Can_copy<int,B*>();
}
// the classical "elements must derived from Mybase*" constraint:
template<class T> class Container : Derived_from<T,Mybase> {
// ...
};
实际上,Derived_from
检查的不是派生,而是转换,但这通常是一个更好的约束。为约束找到好名字可能很困难。
人类如何才能理解这些过于冗长的基于模板的错误消息?
近年来,编译器错误消息已经变得好很多,显示了人类可读的 typedef,并高亮显示了源代码中错误发生的位置。
如果您仍在使用旧编译器,这里有一个免费工具可以将错误消息转换为更易懂的内容。该工具不再开发,但适用于以下编译器: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;
}
为什么我的模板在使用嵌套类型时会出现错误?
或许令人惊讶的是,以下代码不是有效的 C++,即使有些编译器接受它
template<typename Container, typename T>
bool contains(const Container& c, const T& val)
{
Container::iterator iter; // Error, "Container::iterator" is not a type
iter = std::find(c.begin(), c.end(), val);
return iter !=- c.end();
}
原因是理论上,函数模板可以与具有名为 iterator
的数据成员或成员函数的类型一起调用。当编译器解析模板 contains
时,它不知道稍后传入的代码将传递什么类型。这意味着,在编译器知道 Container
是什么以及它有哪些成员之前,无法知道 Container::iterator
是否是一种类型。实际上,C++ 规则规定,除非另有说明,否则编译器必须假定 Container::iterator
不是一种类型。解决方案是通过 typename
关键字向编译器提供提示
template<typename Container, typename T>
bool contains(const Container& c, const T& val)
{
typename Container::iterator iter;
iter = std::find(c.begin(), c.end(), val);
return iter !=- c.end();
}
这告诉编译器,当函数模板稍后使用时,其参数将始终是 Container::iterator
是一种类型的对象。(如果您尝试使用 iterator
是数据成员或其他东西的类型来调用它,您将得到一个错误。)
为什么我的模板派生类在使用从其模板基类继承的嵌套类型时会出错?
或许令人惊讶的是,以下代码不是有效的 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
,因此它们被称为非依赖名称。另一方面,B<T>
依赖于模板参数 T
,因此 B<T>
被称为依赖名称。
规则是:当查找非依赖名称(如 Xyz
或 Pqr
)时,编译器不会在依赖基类(如 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
}
};
为什么我的模板派生类在使用从其模板基类继承的成员时会出错?
或许令人惊讶的是,以下代码不是有效的 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
,因此 f
被称为非依赖名称。另一方面,B<T>
依赖于模板参数 T
,因此 B<T>
被称为依赖名称。
规则是:编译器在查找非依赖名称(如 f
)时,不会在依赖基类(如 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()
是虚函数,这可能无法达到您想要的效果,因为它会抑制虚函数分派机制。
之前的问题会悄无声息地伤害我吗?编译器是否可能悄无声息地生成错误代码?
是的。
由于非依赖类型和非依赖成员在依赖模板基类中找不到,编译器会搜索封闭作用域,例如封闭命名空间。这可能导致它悄无声息地(!)做错事。
例如
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>
继承的实体。
你已经被警告过了。
我如何创建一个容器模板,允许我的用户提供实际存储值的底层容器类型?
首先,让我们澄清问题:目标是创建一个模板 Foo<>
,但其模板参数列表包含某种特定类型的 std::vector<T>
或 std::list<T>
或其他(可能非标准)容器来实际存储值。
这里有一种方法可以做到这一点
template<typename Underlying>
class Foo {
public:
// typename value_type is the type of the values within a Foo-container
typedef typename Underlying::value_type value_type;
void insert(const typename value_type& x)
{
// ...code to insert x into data_...
}
// ...
private:
Underlying data_;
};
Foo<std::vector<int> > x;
Foo<std::list<double> > y;
如果您想允许用户提供不一定具有 value_type
typedef(例如来自第三方的某些容器)的底层容器,您可以明确提供值类型
template<typename T, typename Underlying>
class Foo {
public:
// typename value_type is the type of the values within a Foo-container
typedef T value_type;
void insert(const typename value_type& x)
{
// ...code to insert x into data_...
}
// ...
private:
Underlying data_;
};
Foo<int, std::vector<int> > x;
Foo<double, std::list<double> > y;
然而,您不能(尚不能)将未指定的模板作为模板参数提供,例如这样
template<typename T, template<typename> class Underlying> // Conceptual only; not C++
class Foo {
public:
// ...
private:
Underlying<T> data_; // Conceptual only; not C++
};
Foo<int, std::vector> x; // Conceptual only; not C++
Foo<double, std::list> y; // Conceptual only; not C++
前一个问题的后续:我可以将底层结构和元素类型分开传递吗?
是的,通过一个“代理”技巧。
问题是:std::vector
模板可以有,而且确实有,不止一个参数。您需要让它们在数量、顺序和性质——类型/非类型等方面匹配。
然而,通过“欺骗”来避免指定所有这些参数并使用默认值是可能的。这被称为“代理模板”技术
#include <vector>
#include <list>
template<typename T>
struct wrap_vector {
typedef std::vector<T> type;
};
template<typename T>
struct wrap_list {
typedef std::list<T> type;
};
template<typename T, template<typename> class C>
struct A {
typename C<T>::type data; // trick to use a proxy
};
int main()
{
A<int,wrap_vector> avi;
A<double,wrap_list> adl;
// ...
}
如果您的模板只接受一个参数,您也可以创建一个代理
template<typename T>
struct wrap_my1argtemplate {
typedef my1argtemplate<T> type;
};
“模板 typedef”提案将允许像我们处理类型一样重新定义模板,这将使代理技巧变得不必要。在此之前,请使用类似上述的方法。
相关:所有这些代理都必然会对我的程序速度产生负面影响。是吗?
不是。
它们可能需要额外的一微秒来编译,但是一旦所有类型都被编译器解析,生成的代码的速度与您直接使用它们而没有任何代理模板一样快。
不仅如此,还有一些技术(模板元编程或TMP)在适当的情况下可以提高生成代码的效率。其基本思想是让编译器在编译时做更多的工作,从而在运行时减少工作量。