C++模板

别名模板

template<typename T>
using DequeStack = Stack<T, std::deque<T>>;

那么DequeStack和 Stack<int, std::deque>代表的是同一种类型。

推断指引

推断指引是用来提供额外的模板参数推断规则,或者修正已有的模板参数推断规则。
比如你可以定义,当传递一个字符串常量或者 C 类型的字符串时,应该用 std::string实例化 Stack 模板类:
Stack( char const*) -> Stack<std::string>;
这个指引语句必须出现在和模板类的定义相同的作用域或者命名空间内。通常它紧跟着模板
类的定义。->后面的类型被称为推断指引的”guided type”。
现在,根据这个定义:
Stack stringStack{"bottom"}; // OK: Stack<std::string> deduced since C++17

聚合类的模板化

聚合类(这样一类 class 或者 struct:没有用户定义的显式的,或者继承而来的构造函数,没有 private 或者 protected 的非静态成员,没有虚函数,没有 virtual,private 或者 protected的基类)也可以是模板。比如:

template<typename T>
struct ValueWithComment {
	T value;
	std::string comment;
};

模板的非类型参数

类模板的非类型参数

template<typename T, std::size_t Maxsize>
class Stack {
private:
	std::array<T,Maxsize> elems; // elements …
};

其中std::size_t Maxsize就是一个非类型的模板参数。
对非类型模板参数,也可以指定默认值:

template<typename T = int, std::size_t Maxsize = 100>
class Stack { …
};

但是从程序设计的角度来看,这可能不是一个好的设计方案。默认值应该是直观上正确的。不过对于一个普通的 Stack,无论是默认的 int 类型还是 Stack 的最大尺寸 100,看上去都不够直观。因此最好是让程序员同时显式地指定两个模板参数,这样在声明的时候这两个模板参数通常都会被文档化。

函数模板的非类型参数

template<int Val, typename T>
T addValue (T x)
{
	return x + Val;
}

可以基于前面的模板参数推断出当前模板参数的类型。
比如可以通过传入的非类型模板参数推断出返回类型:

template<auto Val, typename T = decltype(Val)>
T foo();

非类型模板参数的限制

使用非类型模板参数是有限制的。通常它们只能是整形常量(包含枚举),指向objects/functions/members 的指针,objects 或者 functions 的左值引用,或者是 std::nullptr_t(类型是 nullptr)。
浮点型数值或者 class 类型的对象都不能作为非类型模板参数使用。

变参模板

sizeof…运算符

这样可能会让你觉得,可以不使用为了结束递归而重载的不接受参数的非模板函数 print(),只要在没有参数的时候不去调用任何函数就可以了:

template<typename T, typename… Types>
void print (T firstArg, Types… args)
{
	std::cout << firstArg << ’\n’;
	if (sizeof…(args) > 0) { //error if sizeof…(args)==0
		print(args…); // and no print() for no arguments declared
	}
}

但是这一方式是错误的,因为通常函数模板中 if 语句的两个分支都会被实例化。是否使用被实例化出来的代码是在运行期间(run-time)决定的,而是否实例化代码是在编译期间(compile-time)决定的。因此如果在只有一个参数的时候调用 print()函数模板,虽然 args… 为空,if 语句中的 print(args…)也依然会被实例化,但此时没有定义不接受参数的 print()函数,因此会报错。

折叠表达式

since C++ 17

外部模板

为什么需要外部模板

extern说明

“外部模板”是C++11中一个关于模板性能上的改进。实际上,“外部”(extern)这个概念早在C的时候已经就有了。通常情况下,我们在一个文件中a.c中定义了一个变量int i,而在另外一个文件b.c中想使用它,这个时候我们就会在没有定义变量i的b.c文件中做一个外部变量的声明。比如:

        extern int i;

这样做的好处是,在分别编译了a.c和b.c之后,其生成的目标文件a.o和b.o中只有i这个符号的一份定义。具体地,a.o中的i是实在存在于a.o目标文件的数据区中的数据,而在b.o中,只是记录了i符号会引用其他目标文件中数据区中的名为i的数据。这样一来,在链接器(通常由编译器代为调用)将a.o和b.o链接成单个可执行文件(或者库文件)c的时候,c文件的数据区也只会有一个i的数据(供a.c和b.c的代码共享)。
而如果b.c中我们声明int i的时候不加上extern的话,那么i就会实实在在地既存在于a.o的数据区中,也存在于b.o的数据区中。那么链接器在链接a.o和b.o的时候,就会报告错误,因为无法决定相同的符号是否需要合并。

函数模板

而对于函数模板来说,现在我们遇到的几乎是一模一样的问题。不同的是,发生问题的不是变量(数据),而是函数(代码)。这样的困境是由于模板的实例化带来的。
比如,我们在一个test.h的文件中声明了如下一个模板函数:

        template <typename T> void fun(T) {}

在第一个test1.cpp文件中,我们定义了以下代码:

        #include "test.h"
        void test1() { fun(3); }

而在另一个test2.cpp文件中,我们定义了以下代码:

        #include "test.h"
        void test2() { fun(4); }

由于两个源代码使用的模板函数的参数类型一致,所以在编译test1.cpp的时候,编译器实例化出了函数fun(int),而当编译test2.cpp的时候,编译器又再一次实例化出了函数fun(int)。那么可以想象,在test1.o目标文件和test2.o目标文件中,会有两份一模一样的函数fun(int)代码。
代码重复和数据重复不同。数据重复,编译器往往无法分辨是否是要共享的数据;而代码重复,为了节省空间,保留其中之一就可以了(只要代码完全相同)。事实上,大部分链接器也是这样做的。在链接的时候,链接器通过一些编译器辅助的手段将重复的模板函数代码fun(int)删除掉,只保留了单个副本。这样一来,就解决了模板实例化时产生的代码冗余问题。
在这里插入图片描述
对于源代码中出现的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要移除重复的实例化代码。很明显,这样的工作太过冗余,而在广泛使用模板的项目中,由于编译器会产生大量冗余代码,会极大地增加编译器的编译时间和链接时间。解决这个问题的方法基本跟变量共享的思路是一样的,就是使用“外部的”模板。

外部模板声明

        extern template void fun<int>(int);

那么回到一开始我们的例子,来修改一下我们的代码。首先,在test1.cpp做显式地实例化:

        #include "test.h"
        template void fun<int>(int); // 显示地实例化
        void test1() { fun(3); }

那么回到一开始我们的例子,来修改一下我们的代码。首先,在test1.cpp做显式地实例化:

        #include "test.h"
        extern template void fun<int>(int); // 外部模板的声明
        void test1() { fun(3); }

这样一来,在test2.o中不会再生成fun(int)的实例代码。整个模板的实例化流程如图所示:
在这里插入图片描述
这里也可以把外部模板声明放在头文件中,这样所有包含test.h的头文件就可以共享这个外部模板声明了。这一点跟使用外部变量声明是完全一致的。

外部模板使用注意

在使用外部模板的时候,我们还需要注意以下问题:
如果外部模板声明出现于某个编译单元中,那么与之对应的显示实例化必须出现于另一个编译单元中或者同一个编译单元的后续代码中;外部模板声明不能用于一个静态函数(即文件域函数),但可以用于类静态成员函数(这一点是显而易见的,因为静态函数没有外部链接属性,不可能在本编译单元之外出现)。
只有在项目比较大的情况下。我们才建议用户进行这样的优化。总的来说,就是在既不忽视模板实例化产生的编译及链接开销的同时,也不要过分担心模板展开的开销。
参考:深入理解C++11:C++11新特性,2.12节

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值