C++ 编程/模板
模板是一种使代码更可重用的方法。简单的例子包括创建可以存储任意数据类型的通用数据结构。模板对程序员非常有用,尤其是在结合多个 继承 和 运算符重载 时。 标准模板库 (STL) 在连接模板的框架内提供了许多有用的函数。
由于模板非常有表现力,因此它们可以用于除泛型编程之外的其他用途。一种这样的用途称为 模板元编程,它是在编译时而不是运行时预先评估部分代码的一种方法。这里进一步讨论仅与模板作为泛型编程方法有关。
到目前为止,你应该已经注意到执行相同任务的函数往往看起来很相似。例如,如果你编写了一个打印 int 的函数,则必须先声明 int。这样可以减少代码中的错误可能性,但是,必须创建函数的不同版本来处理你使用的所有不同数据类型,这会让人感到有点烦人。例如,你可能希望该函数仅打印输入变量,无论该变量是什么类型。为每种可能的输入类型编写一个不同的函数(double, char *等)将非常繁琐。这就是模板的用武之地。
模板解决了与宏相同的一些问题,在编译时生成“优化”的代码,但受 C++ 的严格类型检查的约束。
参数化类型,更广为人知的是模板,允许程序员创建一个可以处理多种不同类型的函数。你无需考虑每种数据类型,而只需使用一个任意的参数名称,编译器随后会用你希望函数使用、操作等的不同数据类型替换该名称。
- 模板在编译时使用源代码进行实例化。
- 模板是类型安全的。
- 模板允许用户定义的专门化。
- 模板允许非类型参数。
- 模板使用“延迟结构约束”。
- 模板支持混合。
- 模板语法
模板非常易于使用,只需查看语法即可
template <class TYPEPARAMETER>
(或等效地,并被某些人更喜欢)
template <typename TYPEPARAMETER>
有两种类型的模板。函数模板的行为类似于可以接受多种不同类型参数的函数。例如,标准模板库包含函数模板max(x, y)它返回x或y,以较大者为准。max()可以这样定义
template <typename TYPEPARAMETER>
TYPEPARAMETER max(TYPEPARAMETER x, TYPEPARAMETER y)
{
if (x < y)
return y;
else
return x;
}
此模板可以像函数一样调用
std::cout << max(3, 7); // outputs 7
编译器通过检查参数来确定这是一个对max(int, int)的调用,并实例化函数的版本,其中类型TYPEPARAMETER为int.
这无论参数x和y是整数、字符串还是任何其他类型(对于这些类型,可以说“x < y”)都适用。如果你定义了自己的数据类型,则可以使用运算符重载来定义你类型的 <
的含义,从而允许你使用max()函数。虽然这在孤立的例子中似乎是一个微不足道的优势,但在 STL 这样的综合库的背景下,它允许程序员仅通过为新数据类型定义几个运算符来获得广泛的功能。仅仅定义<允许使用标准的类型sort(), stable_sort()和binary_search()算法;set
、堆和关联数组等数据结构;等等。
作为反例,标准类型complex没有定义<运算符,因为 复数 没有严格的顺序。因此,如果 x 和 y 为complex值,则 max(x, y)
会导致编译错误。同样,其他依赖于<的模板无法应用于complex数据。不幸的是,编译器历史上针对这种类型的错误生成了相当晦涩难懂且无益的错误消息。确保某个对象符合 方法协议 可以缓解这个问题。
{TYPEPARAMETER}只是一个任意的 TYPEPARAMETER 名称,你希望在函数中使用它。一些程序员更喜欢只使用T来代替TYPEPARAMETER.
假设你想要创建一个可以处理多个数据类型的交换函数......类似于这样
template <class SOMETYPE>
void swap (SOMETYPE &x, SOMETYPE &y)
{
SOMETYPE temp = x;
x = y;
y = temp;
}
你看到的函数与任何其他交换函数非常相似,不同之处在于函数定义之前的模板 <class SOMETYPE> 行以及代码中的 SOMETYPE 实例。在通常需要使用你使用的类型名称或类的任何地方,你现在用你在模板 <class SOMETYPE> 中使用的任意名称替换它。例如,如果你使用 SUPERDUPERTYPE 而不是 SOMETYPE,代码将如下所示
template <class SUPERDUPERTYPE>
void swap (SUPERDUPERTYPE &x, SUPERDUPERTYPE &y)
{
SUPERDUPERTYPE temp = x;
x = y;
y = temp;
}
如你所见,你可以使用你想要的任何标签作为模板 TYPEPARAMETER,只要它不是保留字即可。
类模板将相同的概念扩展到类。类模板通常用于创建通用容器。例如,STL 有一个 链表 容器。要创建一个整数链表,可以编写list<int>。字符串链表表示为list<string>。一个list具有一组与其关联的标准函数,无论你在方括号之间放置什么,这些函数都能正常工作。
如果你想要拥有多个模板 TYPEPARAMETER,则语法将是
template <class SOMETYPE1, class SOMETYPE2, ...>
- 模板和类
假设你想要创建的不是一个简单的模板函数,而是希望将模板用于类,以便该类可以处理多个数据类型。你可能已经注意到,某些类能够接受类型作为参数并根据该类型创建对象的变体(例如 STL 容器类层次结构的类)。这是因为它们被声明为模板,使用的语法与下面介绍的语法类似
template <class T> class Foo
{
public:
Foo();
void some_function();
T some_other_function();
private:
int member_variable;
T parametrized_variable;
};
定义模板类的成员函数有点像定义函数模板,除了你需要使用作用域解析运算符来指示这是模板类的成员函数。唯一一个重要且不明显的细节是需要在类名之后使用包含参数化类型名称的模板运算符。
以下示例通过定义来自上述示例类的函数来描述所需的语法。
template <class T> Foo<T>::Foo()
{
member_variable = 0;
}
template <class T> void Foo<T>::some_function()
{
cout << "member_variable = " << member_variable << endl;
}
template <class T> T Foo<T>::some_other_function()
{
return parametrized_variable;
}
如你所见,如果你想要声明一个将返回参数化类型对象的函数,你只需将该参数的名称用作函数的返回值类型即可。
模板的一些用途,例如max()函数,以前由类似函数的 预处理器 宏 填补。
// a max() macro
#define max(a,b) ((a) < (b) ? (b) : (a))
宏和模板都在编译时展开。宏始终内联展开;模板也可以在编译器认为合适时内联展开。因此,类似函数的宏和函数模板都没有运行时开销。
但是,模板通常被认为是这些用途的宏的改进。模板是类型安全的。模板避免了在大量使用类似函数的宏的代码中发现的一些常见错误。也许最重要的是,模板旨在适用于比宏更大的问题。类似函数的宏的定义必须适合一行代码。
使用模板有三个主要缺点。首先,许多编译器历史上对模板的支持非常糟糕,因此使用模板会使代码的可移植性略有降低。其次,几乎所有编译器在模板代码中检测到错误时都会生成令人困惑且无用的错误消息。这使得模板难以开发。第三,模板的每次使用都可能导致编译器生成额外的代码(模板的实例化),因此不加区分地使用模板会导致 代码膨胀,从而导致可执行文件过大。
模板的另一个主要缺点是,无法用与不同类型或函数调用相同方式起作用的 #define(如 max)进行替换。模板已经取代了使用 #define 用于复杂函数,但没有用于简单的东西,如 max(a,b)。有关尝试为 #define max 创建模板的完整讨论,请参阅 Scott Meyer 于 1995 年 1 月为C++ Report 撰写的论文 "Min, Max and More"。
使用模板的最大优势是,一个复杂的算法可以有一个简单的接口,然后编译器使用该接口根据参数的类型选择正确的实现。例如,搜索算法可以利用正在搜索的容器的属性。这种技术在整个 C++ 标准库中都有使用。
在链接一个由多个模块组成的模板程序时,这些模块分散在多个文件中,经常会遇到一个令人困惑的问题:模块的代码无法链接,因为出现了“未解析的引用到(插入模板成员函数名称)在(...)中”。出错的函数实现就在那里,那为什么它会从目标代码中丢失呢?让我们停下来思考一下,这怎么可能呢?
假设您创建了一个名为 Foo 的模板类,并将它的声明放在 Util.hpp 文件中,以及一些其他的普通类,比如 Bar。
template <class T> Foo
{
public:
Foo();
T some_function();
T some_other_function();
T some_yet_other_function();
T member;
};
class Bar
{
Bar();
void do_something();
};
现在,为了遵循所有艺术规则,您创建了一个名为 Util.cc 的文件,在其中放置所有函数定义,无论是模板还是其他。
#include "Util.hpp"
template <class T> T Foo<T>::some_function()
{
...
}
template <class T> T Foo<T>::some_other_function()
{
...
}
template <class T> T Foo<T>::some_yet_other_function()
{
...
}
最后,
void Bar::do_something()
{
Foo<int> my_foo;
int x = my_foo.some_function();
int y = my_foo.some_other_function();
}
接下来,您编译模块,没有错误,您很高兴。但是假设程序中还有另一个(主)模块,位于 MyProg.cc 中。
#include "Util.hpp" // imports our utility classes' declarations, including the template
int main()
{
Foo<int> main_foo;
int z = main_foo.some_yet_other_function();
return 0;
}
它也干净地编译到目标代码。但是,当您尝试将这两个模块链接在一起时,您会收到一个错误,提示在 MyProg.cc 中有一个未定义的引用到 Foo<int>::some_yet_other function()。您已经正确地定义了模板成员函数,那么问题是什么呢?
您还记得,模板是在编译时实例化的。这有助于避免代码膨胀,因为如果为所有可能的类型及其参数生成所有模板类和函数的变体,将会导致代码膨胀。因此,当编译器处理 Util.cc 代码时,它看到 Foo 类的唯一变体是 Foo<int>,并且唯一需要的函数是
int Foo<int>::some_function();
int Foo<int>::some_other_function();
Util.cc 中的代码不需要任何其他变体的 Foo 或其方法,所以编译器没有生成除了这些代码之外的其他代码。目标代码中没有 some_yet_other_function() 的实现,就像没有以下实现一样:
double Foo<double>::some_function();
或
string Foo<string>::some_function();
MyProg.cc 代码编译没有错误,因为 Foo 中使用的成员函数在 Util.hpp 头文件中正确声明,并且预计在链接时它会可用。但它没有,因此出现了错误,如果你是模板新手,你可能会开始在代码中寻找错误,而讽刺的是,你的代码是完全正确的。
解决方案在一定程度上取决于编译器。对于 GNU 编译器,尝试使用 -frepo 标志进行实验,并阅读 “info gcc”(节点 "模板实例化":"模板在哪里?")中关于模板的部分可能会有所启发。据说在 Borland 中,链接器选项中有一个选项,可以为这类问题激活“智能”模板。
您还可以尝试另一种方法,称为显式实例化。您要做的是在包含模板的模块中创建一些虚拟代码,这些代码会创建模板类的所有变体,并调用所有已知在其他地方需要的成员函数变体。显然,这要求您了解整个代码中需要哪些变体。在我们简单的示例中,这将是这样的
1. 在 Util.hpp 中添加以下类声明
class Instantiations
{
private:
void Instantiate();
};
2. 在 Util.cc 中添加以下成员函数定义
void Instantiations::Instantiate()
{
Foo<int> my_foo;
my_foo.some_yet_other_function();
// other explicit instantiations may follow
}
当然,您永远不需要实际实例化 Instantiations 类,或调用其任何方法。它们的存在本身就会让编译器生成所有需要的模板变体。现在,目标代码将能够链接,不会出现问题。
还有一种解决方案,虽然不优雅,但也行之有效。将所有模板函数的定义代码移到 Util.hpp 头文件中。这样做不太好,因为头文件用于声明,而实现应该在其他地方定义,但这在这种情况中可以解决问题。在编译 MyProg.cc(以及包含 Util.hpp 的任何其他模块)代码时,编译器会生成所有需要的模板变体,因为定义可以直接使用。