C++的模板编程是学习C++不可或缺的一部分,说来讽刺,这部分在Rust中实现得很优雅.
函数模板
1 | //C++ 提供了不同的方法来处理这个问题: |
模板参数推导
自动类型转换在类型推导时有一些限制:
- 当通过引用声明参数时,简单的转换也不适用于类型推导。用同一个模板参数T声明的两个实参必须完全匹配。
- 当按值声明参数时,只支持简单的转换:忽略const或volatile的限定符,引用转换为引用的类型,原始数组或函数转换为相应的指针类型。对于使用相同模板参数T声明的两个参数,转换类型必须匹配。
与普通函数一样,函数模板也可以重载。可以使用相同的函数名来声明不同的函数体,当使用 该函数名称时,编译器会决定调用哪一个候选函数。
重载函数模板时,应该确保只有一 个函数模板与调用匹配。
类模板
1 |
|
模板聚合
聚合类(不由用户提供、显式或继承的构造函数的类/结构,没有private或protected的非静态数据成员,没有虚函数,也没有virtual、private或protected基类)也可以是模板1
2
3
4
5
6
7
8template<typename T>
struct ValueWithComment {
T Value;
std::string comment;
};
ValueWithComment<int> vc;
vc.value = 42;
vc.comment = "initial value";
非类型模板参数
1 | template<typename T, std::size_t Maxsize> |
函数模板非类型参数
1 | template<int Val,typename T> |
非类型模板参数限制
非类型模板参数有一些限制,只能是整型常量值(包括枚举),指向对象/函数/成员的指针,指 向对象或函数的左值引用,或者std::nullptr_t(nullptr的类型)
- 浮点数和类型对象不允许作为非类型模板参数
- 当向指针或引用传递模板参数时,对象不能是字符串字面值、临时对象或数据成员和其他子对象。
非类型模板参数可以是编译时表达式
模板参数类型auto
1 |
|
- 模板的模板参数可以是值,而非类型。
- 不能将浮点数或类类型对象作为非类型模板的参数。对于指向字符串字面量、临时对象和子 对象的指针/引用,有一些限制。
- 使用auto可使模板具有泛型值的非类型模板参数。
可变参数模板
1 | template<typename T,typename... Types> |
重载可变和非可变模板
1 | template<typename T> |
若两个函数模板的区别仅在于末尾参数包的不同,则首选没有末尾参数包的函数模板
sizeof...
操作符应用在可变参模板
折叠表达式
1 | template<typename... T> |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32struct Node {
int value;
Node* left;
Node* right;
Node(int i=0):value(i),left(nullptr),right(nullptr){}
};
auto left = &Node::left;
auto right = &Node::right;
template<typename T,typename... TP>
Node* traverse(T np,TP... paths){
return (np ->* ... ->* paths);
}
template<typename... Types>
void printargs(Types const&... args) {
(std::cout<< ... << args)<<'\n';
}
template<typename T>
class AddSpace {
private:
T const& ref;
public:
AddSpace(T const& r):ref(r) {
}
friend std::ostream& operator<< (std::ostream& os,AddSpace<T> s) {
return os<< s.ref<<' ';
}
};
template<typename... Args>
void print(Args... args){
(std::cout<< ... <<AddSpace(args))<<'\n';
可变参数模板在实现通用库(如C++标准库)时扮演着重要的角色。 典型的应用是转发可变数量的类型参数
类模板和表达式
1 | template<typename... Args> |
表达式
1 | template<typename... Args> |
索引
1 | template<typename C,typename... Idx> |
基础技巧
typename1
2
3
4
5
6
7
8
9template<typename T>
void printcoll(T const& coll) {
typename T::const_iterator pos;
typename T::const_iterator end(coll.end());
for(pos = coll.begin();pos!=end;++pos) {
std::cout<<*pos<<' ';
}
std::cout<<'\n';
}
说明模板内的标识符是类型.当模板参数是类型时,必须使用typename。
零值初始化
因此,可以显式调用内置类型的默认构造函数,该构造函数用0初始化内置类型(bool为false, 指针为nullptr)。因此,即使是内置类型,也可以通过零初始化来确保正确的初始化
使用this->
对于具有依赖于模板参数的基类类模板,即使成员x被继承,使用名称x本身并不总是等同于 this->x。
原始数组和字符串字面量的模板
成员模板
1 | template<typename T> |
1 | template<typename T,typename Cont=std::deque<T>> |
成员函数模板也可以全特化,不能偏特化1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class BoolString {
private:
std::string value;
public:
BoolString (std::string const& s):value(s) {
}
template<typename T= std::string>
T get() const {
return value;
}
};
template<>
inline bool BoolString::get<bool>() const {
return value == "true" || value =="1" || value == "on";
}
不需要也不能对特化进行声明,只需要定义。因为它是全特化的,且位于头文件中,所以若定 义包含在不同的转译单元中,必须使用内联声明,以避免错误。
特殊成员函数的模板
只要特殊成员函数允许复制或移动对象,就可以使用模板成员函数。与上面定义的赋值操作符 类似,也可以是构造函数。但模板构造函数或模板赋值操作符,不能取代预定义构造函数或赋值操 作符,成员模板不作为复制或移动对象的特殊成员函数。本例中,对于相同类型的堆栈赋值,仍然使用默认赋值操作符
泛型Lambda和成员模板
1 | auto sum = [](auto x,auto y) { |
变量模板
1 | // 变量模板 |
std::enable_if<>和移动语义
1 | class X{ |
从C++11开始,标准库提供了辅助模板std::enable_if<>,以在特定的编译时条件下忽略函数模板
std::enable_if<> 是一种类型特征,计算作为其(第一个)模板参数传递的给定编译时 表达式:
- 若表达式结果为true,其类型成员类型将产生一个类型:
- 若没有传递第二个模板参数,则该类型为void。否则,该类型就是第二个模板参数类型
若表达式结果为false,则没有定义成员类型。由于SFINAE的模板特性(替换失败不为 过),这将忽略使用enable_if表达式的函数模板
在声明中间使用enable_if表达式非常笨拙。由于这个原因,使用std::enable_if<>的常见方法是 使用带有默认值的函数模板参数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18template<typename T>
typename std::enable_if_t<(sizeof(T)>1),T> foo() {
return T{}*2;
}
template<typename T,typename = std::enable_if_t<(sizeof(T)>4)>>
void foo() {
}
template<typename T>
typename std::enable_if_t<(sizeof(T)>1),T> foo() {
return T{}*2;
}
template<typename T,typename = std::enable_if_t<(sizeof(T)>4)>>
void foo() {
}
禁用模板构造函数1
2
3
4
5
6
7
8
9
10
11
12
13
14class Person {
private:
std::string name;
public:
template<typename STR,typename = std::enable_if_t<std::is_convertible_v<STR,std::string> >>
explicit Person(STR&& n):name(std::forward<STR>(n)){}
Person(Person const& p):name{p.name}{
}
Person(Person &&p):name{std::move(p.name)}{
}
}
使用概念简化std::enable_if表达式
1 | template<typename T> |
按值与按引用传递
按值传递
按值传递参数时,原则上必须复制每个参数,每个参数都成为所传递实参的副本。对于类,作 为副本创建的对象通常由复制构造函数初始化。 调用复制构造函数的代价可能会很高。然而,即使在按值传递参数时,也有方法来避免复制: 编译器可能会优化复制对象的复制操作,并且通过移动语义,对复杂对象的操作也可以变得廉价1
2
3
4
5
6
7template<typename T>
void printV(T arg) {
}
class Student{};
Student s;
printV(std::move(s));
按值传递:1. 会进行拷贝/移动构造,相当于进行了一次拷贝 2. 会decay衰变::当按值传递参数时,类型会衰变。从而数组将转换为指针,并删除 const 和 volatile 等限定符(就像使用值作为使用auto声明的对象的初始化式一样)
优点: 方便
缺点: 多一次构造并且参数会衰变,影响数组和指针
按引用传递
常量引用
为了避免(不必要的)复制,传递非临时对象时,可以使用常量引用
传递引用类型不会衰变 当通过引用将参数进行传递时,就不会衰变。从而不会将数组转换为指针,并且不删除const和 volatile等限定符。因为调用参数声明为Tconst&,所以模板参数T本身并没有推导为const。
优点:避免(不必要的)复制
缺点:通过引用传递参数是通过传递参数地址实现的。地址编码紧凑时,将地址从调用 方传递给被调用方的效率很高。然而,在编译代码时,传递地址会给编译器带来不确定性
传递非常量引用
当通过传递的参数作为返回值时(例如,使用out或inout参数时),必须使用非常量引用(除非 通过指针传递)。在传递参数时,不会创建副本,被调用函数模板的参数只能直接访问传递的参数1
2
3
4template<typename T>
void OutR(T& arg) {
...
}
不允许对一个临时的(prvalue)或一个通过std::move()(xvalue)传递的现有对象调用
如果传递const就会有比较尴尬的情况.。若传递const参数,可能导致arg变成一个常量引用的声明,这意 味着允许传递右值,但这里需要左值.这种情况下,修改函数模板中传递的参数是错误的。在表达式中传递常量对象是可能的,但当 函数完全实例化时(这可能发生在编译的后期),修改该值的尝试都将触发错误
需要使用静态断言或std::enable_if或概念禁用
通过转发引用进行传递
使用引用调用的原因是能够完美地转发参数。但当使用转发引用(定义为模板形参的右值引用)时,需要使用特殊的规则。1
2
3
4template<typename T>
void printV(T&& arg){
}
将参数声明为转发引用几乎完美。但是注意传左值时,T推到为左引用不能用来直接初始化值.
std::ref()和std::cref()
可以让调用者决定函数模板参数是通过值传递,还是通过引用传递。当模板声明为 按值接受参数时,调用者可以使用在头文件中声明的std::cref()和std::ref(),通过引用 传递参数。 它们都是将对象包装为引用,本身不能直接与其他类型比较,往往需要一个转换.
编译时编程
使用constexpr计算1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46template<unsigned p,unsigned d>
struct DoIsPrime {
static bool constexpr value = (p%d != 0) && DoIsPrime<p,d-1>::value;
};
//偏特化
template<unsigned p>
struct DoIsPrime<p,2> {
static constexpr bool value = (p%2 != 0);
};
//重载
template<unsigned p>
struct IsPrime{
static constexpr bool value = DoIsPrime<p,p/2>::value;
};
template<>
struct IsPrime<0> {
static constexpr bool value = false;
};
template<>
struct IsPrime<1> {
static constexpr bool value = false;
};
template<>
struct IsPrime<2> {
static constexpr bool value = true;
};
template<>
struct IsPrime<3> {
static constexpr bool value = true;
};
constexpr bool doIsPrime(unsigned p,unsigned d) {
return d!=2 ? (p%d!=0) && doIsPrime(p,d-1):(p%2!=0);
}
constexpr bool longisPrime(unsigned int p) {
for (unsigned int d=2;d<=p/2;d++) {
if(p%d==0){
return false;
}
return p>1;
}
使用偏特化的执行路径选择1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19template<int SZ,bool = isPrime(SZ)>
struct Helper;
template<int SZ>
struct Helper<SZ,false> {
};
template<int SZ>
struct Helper<SZ,true> {
};
template<typename T,std::size_t SZ>
long foo(std::array<T,SZ> const& coll) {
Helper<SZ> h;
}
因为函数模板不支持偏特化,所以必须使用其他机制根据某些约束来更改函数实现。可供的选择包括:
- 带有静态函数的类
- std::enable_if
- SFINAE特性
- 编译时if特性
SFINAE
以各种参数类型重载的函数很常见。因此,当编译器看到对重载函数的调用时,必须 考虑每个候选函数,评估调用参数,并选择最匹配的候选函数。 候选集包括函数模板的情况下,编译器首先必须确定为该候选对象使用哪些模板参数,然后在 函数参数列表及其返回类型中替换这些参数,然后评估匹配程度(就像普通函数一样)。但替换过程 可能会遇到问题:可能产生毫无意义的构造。语言规则并不认为这种无意义的替换会导致错误,而 具有这种问题的候选则会直接忽略。1
2
3
4
5
6
7
8
9
10
11template<typename T>
void foot(T t) {
if constexpr (std::is_integral_v<T>) {
if(t>0){
foo (t-1);
}
else{
static_assert(!std::is_integral_v<T>,"no integral");
}
}
}
实际使用模板
包含模型
为了实例化模板,编译器必须知道应该实例化哪个定义,以及应该为哪个模板参数实例化它。常见的解决方案是使用与宏或内联函数相同的方法:在声明模板的头文件中包 含模板的定义。
这在实践中是一个问题,因为它增加了编译器编译重要程序所需的时间。因此,我们将研究一 些方法来解决这个问题,包括预编译头文件和使用显式模板实例化
通用库设计
C++ 中,有几种类型可以很好地用于回调,它们既可以作为函数调用参数传递,也可以以f(… ) 方式直接使用:
- 函数指针类型
- 具有重载operator()(函数操作符,有时称为函子)的类类型,包括Lambda
- 使用转换函数生成指向函数的指针或指向函数引用的类类型 这些类型统称为函数对象类型,这种类型的值就是函数对象
std::invoke
1 |
|
类型特征
1 | template<typename T> |
std::addressof
std::addressof<>() 函数模板生成对象或函数的实际地址。即使对象类型有重载操作符&,也能 工作。尽管后者很少使用,但可能会发生。因此,如果需要任意类型对象的 地址,建议使用addressof()
std::declval
1 | template<typename T1,typename T2, typename RT=std::decay_t<decltype(true?std::declval<T1>():std::declval<T2>())>> |
std::declval<>() 函数模板可以用作特定类型的对象引用的占位符。该函数没有定义,因此不能 调用(也不创建对象)。因此,只能用于未求值的操作数(decltype和sizeof构造的操作数)。因此,与 其尝试创建一个对象,可以假设有一个相应类型的对象。
不过,这只可能在decltype未求值的上下文中实 现。 不要忘记使用std::decay<>类型来确保默认的返回类型不是一个引用,因为std::declval()本身 会产生右值引用。否则,像max(1,2)这样的调用将得到一个int&&的返回类型
完美转发临时变量
1 | template<typename T> |
模板参数的引用
使用decltype(auto)可以很容易地产生引用类型,因此在上下文中最好不要使用。
开发者应该注意 decltype 产生类型的差别,这取决于传递的参数是声明还是表达式: • decltype(e) 中,若e 是实体 (如变量、函数、枚举器或数据成员) 或类成员访问的名称,则 decltype(e) 生成该实体声明的类型或指定的类成员。因此,decltype可以用来检查变量的类型。 这用在需要精确匹配现有声明的类型时。否则,若e是其他表达式,decltype(e)会产生一个类型,表示该表达式的类型和值别
- 若e是类型T的左值,则decltype(e)生成T&。
- 若e是类型T的xvalue,则decltype(e)生成T&&。
- 若e是类型T的prvalue,则decltype(e)生成T
1 | void check_ref(std::string&& s) { |
auto使用模板参数推导规则来确定感兴趣的类型,实际的类型是通过直接对表达式应用decltype来确定