C++模板元编程详细教程(之三)-创新互联
先序文章请看
C++模板元编程详细教程(之一)
C++模板元编程详细教程(之二)
有了前两篇的基础,相信大家对模板编程已经有一点初步的感觉了。趁热打铁,这一篇我们主要来介绍一下模板特化。
首先来看一下下面的例子:
templatevoid add(T &t1, const T &t2) {t1 += t2;
}
上面是一个简单的模板函数,用于把第二个参数的值加到第一个参数中去。这个模板函数对于基本数据类型的实例化都是没什么问题的,但是如果是字符串的话,那将会有问题:
void Demo() {int a = 1, b = 3;
add(a, b); // add,调用符合预期
char c1[16] = "abc";
char c2[] = "123";
add(c1, c2); // add, 调用不符合预期
}
这里的问题就在于,对于字符串类型(这里指原始C字符串,而不是std::string
)来说,「相加」并不是简单的+=
,因为字符串主要是用字符指针来承载的,指针相加是不合预期的。我们希望的是字符串拼接。
因此,我们希望,单独针对于char *
的实例化能够拥有不同的行为,而不遵从「通用模板」中的定义。这种语法支持就叫做「特化」,或「特例」。可以理解为,针对于模板参数是某种特殊情况下进行的特殊实现。
因此,我们在通用模板的定义基础上,再针对char *
类型定义一个特化:
#includetemplatevoid add(T &t1, const T &t2) {t1 += t2;
}
template<>// 模板特化也要用模板前缀,但由于已经特化了,所以参数为空
void add(char *&t1, char *const &t2) {// 特化要指定模板参数,模板体中也要使用具体的类型
std::strcat(t1, t2);
}
void Demo() {int a = 1, b = 3;
add(a, b); // add是通过通用模板生成的,因此本质是a += b,符合预期
char c1[16] = "abc";
char c2[] = "123";
add(c1, c2); // add有定义特化,所以直接调用特化函数,因此本质是strcat(c1, c2),符合预期
}
上例简单展示了一下模板特化目标解决的问题,和其基本的语法。但其实模板特化远不止如此,它有着巨大的潜力。
模板的特化分两种情况,一种是全特化(有的地方也叫特例),一种是偏特化(有的地方也叫部分特化)。全特化相对简单一些,笔者会先来介绍。而偏特化会伴随SFINAE理论,它将会成为模板元编程最核心的理论基础。
全特化与模板的链接方式首先复习一下我们在开篇时候所提到的一个非常重要的概念。**模板本身不是可使用的代码,而是一种代码的升成方法。需要经过实例化后才能成为实际可用的代码。**比如说模板函数需要指定模板参数(可以是显式指定,也可以是编译器自动推导)实例化后,才能成为函数,同理,模板类也需要实例化后才能成为类。
然而「全特化」就是说,当所有模板参数都指定了的时候,才叫「全」。那么上一节中add
的示例就是一个全特化,因为它原本只有一个模板参数,把它特化了自然是「完全」特化的。
而要谈到全特化,就不得不谈到一个非常容易踩坑的点,那就是模板的链接方式。在一个单独的.cpp文件中使用模板并不会有什么链接性问题,但如果在多个文件中都要使用呢?自然要通过「头文件声明+链接」的方式来完成了。
但模板本身又很特殊,它本身不是可用的代码,而是代码生成器,因此编译器会在编译期用模板来生成代码,注意,这个时候还没有开始链接!所以问题就产生了,假如我们按照直觉和习惯,把模板的声明和定义分文件来写,会怎么样呢?请看下面示例:
tmp.h
#pragma once
templatevoid f(const T &t); // 声明
tmp.cpp
#include "tmp.h"
templatevoid f(const T &t) {} // 实现
main.cpp
#include "tmp.h"
int main() {f(1); // ff(1.0); // freturn 0;
}
如果我们真的这么做了,你会发现链接时会报错。原因是这样的,我们在tmp.h中的这种写法,并不是「声明了一个模板函数」,模板函数本不是函数,是不需要声明的,大家记住模板永远是生成代码的工具。所以tmp.h中的写法是「声明了一组函数」,包括我们在main
函数中使用的f
和f
,之所以能通过编译,就是因为tmp.h中存在它们的声明。换句话说,template
相当于void f
,void f
,void f
……这一系列的「函数声明」。
所以,编译是没问题的,但是链接的时候会报找不到f
和f
的实现。这是为什么呢?明明我在tmp.cpp中实现了呀!那我们来「换位思考一下」,假如你是编译器,我们知道「编译」过程是单文件行为,那么你现在来编译main.cpp,首先进行预处理,把#include
替换成对应头文件内容,那么main.cpp就变成了:
templatevoid f(const T &t);
int main() {f(1); // ff(1.0); // freturn 0;
}
上面的f
是函数声明,下面编译主函数的时候,根据参数推导出了f
和f
,于是,通过上面的「模板函数声明」生成了2条实际的「函数声明」语句,也就是:
void f(const int &);
void f(const double &);
调用都是符合声明的,OK,结束编译,我们得到了main.o。
好了,下面我们来编译tmp.cpp。同理,先做预处理,得到了:
templatevoid f(const T &t);
templatevoid f(const T &t) {}
这时,问题的关键点来了!,这个模板函数f
在当前这个编译单元中,并没有任何实例化,那么你自然就不知道应当按这个模板来生成哪些实例。所以,你只能什么都不做,很无奈地生成了一个空白的tmp.o。
最后,main.o和tmp.o链接,main.o中的f
和f
都找不到实现,所以链接报错。
这就是模板的链接方式问题,由于模板都是编译期进行实例化,因此,必须在编译期就得知道需要哪些实例化,然后把这些实例化后的代码编译出来,再去参与链接,才能保证结果正确。
所以,要保证编译期能知道所有需要的实例,我们只能把模板实现放在头文件里。这样,每一个编译单元都能根据自己需要的实例来生成代码。也就是说,上面的代码应该改造成:
tmp.h
#pragma once
templatevoid f(const T &t);
templatevoid f(const T &t) {} // 当然,文件内部没有声明依赖关系的时候,声明和实现可以合并
main.cpp
#include "tmp.h"
int main() {f(1);
f(1.0);
return 0;
}
这时,在编译main.cpp时,就会把f
和f
的实例都编译出来,这样就不会链接报错了。
但这样会引入另一个问题,如果多个.cpp引入同一个含有模板的.h文件,并做了相同的实例化,会不会生成多份函数实现呢?这样链接的时候不是也会报错吗?
设计编译器的大佬们自然也想到这个问题了,那么解决方法就是,通过模板实例出的内容,会打上一个全局标记,最终链接时只使用一份(毕竟是从同一份模板生成出来的,每一份自然是相同的)。再换句更通俗易懂的说法就是模板实例一定是inline
的,编译器会给每个模板实现自动打上inline
标记,确保链接时全局唯一。
现在我们再回头看一下全特化模板,全特化模板已经是实例化过的了,因此并不会出现编译期不知道要怎么实例化的问题。如果这时我们还把实现放在头文件中会怎么样?
tmp.h
#pragma once
templatevoid f(T t) {} // 通用模板
template<>void f(int t) {} // 针对int的全特化
t1.cpp
#include "tmp.h"
void Demo1() {f(1); // f}
t2.cpp
#include "tmp.h"
void Demo2() {f(1); // f}
我们再来当一次编译期。首先编译t1.cpp,预处理展开,得到了f
的实现,所以把f
编译过来,输出t1.o。同理,编译t2.cpp后,也会有一份f
的实现在t2.o中。最后链接的时候,发现f
重定义了!
因此我们发现,全特化的模板其实已经不是模板了,在这里f
会按照普通函数一样来进行编译和链接。所以直接把实现放在头文件中,就有可能在链接时重定义。解决方法有两种,第一种就是我们手动补上inline
关键字,提示编译期要打标全局唯一。
tmp.h
#pragma once
templatevoid f(T t) {} // 通用模板,编译器用通用模板生成的实例会自动打上inline
template<>inline void f(int t) {} // 针对int的全特化,必须手动用inline修饰后才能在编译期打标保证链接全局唯一
第二种方法就是,当做普通函数处理,我们把实现单独抽到一个编译单元中独立编译,最后在链接时才能保证唯一:
tmp.h
#pragma once
templatevoid f(T t) {} // 通用模板
template<>void f(int t); // 针对int的全特化声明(函数声明)
tmp.cpp
#include "tmp.h"
template<>void f(int t) {} // 函数实现
之后,f
会随着tmp.cpp的编译,单独存在在tmp.o中,最后链接时就是唯一的了。
另外,对于特化的模板函数来说,参数必须是按照通用模板的定义来写的(包括个数、类型和顺序),但对于模板类来说,则没有任何要求,我们可以写一个跟通用模板压根没什么关系的一种特化,比如说:
templatestruct Test {// 通用模板中有2个成员变量,1个成员函数
T a, b;
void f();
};
template<>struct Test{// 特化的内部定义可以跟通用模板完全不同
double m;
static int ff();
}
偏特化偏特化又叫部分特化,既然是「部分」的,那么就不会像全特化那样直接实例化了。偏特化的模板本质上还是模板,它仍然需要编译期来根据需要进行实例化的,所以,在链接方式上来说,全特化要按普通函数/类/变量来处理,而偏特化模板要按模板来处理。
先明确一个点:模板函数不支持偏特化,因此偏特化讨论的主要是模板类。
我们先来看一个最简单的偏特化的例子:
templatestruct Test {};
templatestruct Test{};
上面代码就是针对Test
模板类,第一个参数为int
时的「偏特化」,那么只要是第一个参数为int
的时候,就会按照偏特化模板来进行实例化,否则会按照通用模板进行实例化。为了方便说明,我们在通用模板和偏特化模板中加一些用于验证性的代码:
#includetemplatestruct Test {};
templatestruct Test{static void f();
};
templatevoid Test::f() {std::cout<< "part specialization"<< std::endl;
}
void Demo() {Test::f(); // 按照偏特化实例化,有f函数
Test::f(); // 按照偏特化实例化,有f函数
Test::f(); // 按照通用模板实例化,不存在f函数,编译报错
}
偏特化模板本身仍然是模板,仍然需要经历实例化。但偏特化模板可以指定当一些参数满足条件时,应当按照指定方式进行实例化而不是通用模板定义的方式来实例化。
那如果偏特化和全特化同时存在呢?比如下面的情况:
templatestruct Test {}; // 【0】通用模板
templatestruct Test{}; // 【1】偏特化模板
template<>struct Test{}; // 【2】全特化模板
void Demo() {Testt; // 按照哪个实例化?
}
先说答案,上面的实例会按照【2】的方式,也就是直接调用全特化。大致上来说,全特化优先级高于偏特化,偏特化高于通用模板。
对于函数来说,模板函数不支持偏特化,但支持重载,并且重载的优先级高于全特化。比如说:
void f(int a, int b) {} // 重载函数
templatevoid f(T1 a, T2 b) {} // 通用模板
template<>void f(int a, int b) {} // 全特化
void Demo() {f(1, 2); // 会调用重载函数
f<>(1, 2); // 会调用全特化函数ff(2.5, 2.6); // 会用通用模板生成f}
回到模板类的偏特化上,除了上面那种制定某些参数的偏特化以外,还有一种相对复杂的偏特化,请看示例:
templatestruct Tool {}; // 这是另一个普通的模板类
templatestruct Test {}; // 【0】通用模板
templatestruct Test>{}; // 【1】偏特化
void Demo() {Testt1; // 使用【0】实例化TestTest>; // 使用【1】实例化Test>Test>; // 使用【1】实例化Test>}
有的资料会管上面这种特化叫做「模式特化」,用于区分普通的「部分特化」。但它们其实都属于偏特化的一种,因为偏特化都是相当于特化了参数的范围。在上面的例子中,我们是针对于「参数是Tool
的实例类型」这种情况进行了特化。
所以,偏特化并不一定意味着模板参数数量变小,它有可能不变,甚至有可能是增加的,比如说:
templatestruct Tool {}; // 这是另一个普通的模板类
templatestruct Test {}; // 【0】通用模板
templatestruct Test>{}; // 【1】偏特化模板
templatestruct Test>{}; // 【2】偏特化模板
void Demo() {Testt1; // 【0】
Test>t2; // 【2】
Test>t3; // 【1】
}
所以偏特化的引入,让模板编程这件事有了爆炸性的颠覆,因为其中的组合可以随意发挥想象。但这里就引入了另一个问题,就比如上例中,【1】和【2】都是偏特化的一种,但为什么Test
选择了【2】而不是【1】呢?这么说,看来不仅仅是跟全特化和通用模板存在优先级问题,多种偏特化之间也仍然存在优先级问题,那么编译器究竟是按照什么方式来进行偏特化匹配的呢?这就是我们下一篇要着重研究的问题了。
这一篇我们主要介绍了模板的链接方式和模板的特化,重点希望读者理解和掌握的是模板类的偏特化,因为C++的模板元编程其实就是一系列模板的偏特化来实现各种静态功能的。
下一篇会介绍偏特化的优先级匹配法则和SFINAE特性。
C++模板元编程详细教程(之四)
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
分享名称:C++模板元编程详细教程(之三)-创新互联
链接分享:http://scyanting.com/article/dciieg.html