C++ Template

本文是阅读C++ templates: the complete guide 2nd的笔记。

模板参数不会被推导的情况

1
2
3
4
5
6
7
8
9
10
#include <iostream>
template<typename RT, typename T1, typename T2>
RT max(T1 a, T2 b){
return a > b ? a : b;
}
int main() {
int a = 2;
double b = 3.14;
std::cout << ::max(a, b) << std::endl;
}
1
2
3
4
prog.cpp:3:4: note:   template argument deduction/substitution failed:
prog.cpp:9:25: note: couldn't deduce template parameter ‘RT’
std::cout << ::max(a, b) << std::endl;
^

上面的例子中,RT的类型是T1 or T2,编译器无法推断:

In cases when there is no connection between template parameters(e.g. RT) and call parameters(e.g. a and b), the compiler can’t take the template parameters into account and also can’t deduce it.

RT与函数形参无关时,编译器不会去推断RT的类型。

编译器不是推断不出RT,而是根本不会去推断RT。把代码改成这样,编译器也不会去推断RT:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
template<typename RT, typename T>
RT max(T a, T b){
return a > b ? a : b;
}
int main() {
int a = 2;
int b = 3;
std::cout << ::max(a, b) << std::endl;
}

尽管我们一眼可以看出RT肯定等于T,但编译器不会去推断。

正确示范

使用auto

1
2
3
4
5
6
7
8
9
10
#include <iostream>
template<typename T1, typename T2>
auto max(T1 a, T2 b){
return a > b ? a : b;
}
int main() {
int a = 2;
double b = 3.14;
std::cout << ::max(a, b) << std::endl;
}

auto会在编译期进行推断,例如a为int,b为double,在进行a>b比较时,a会隐式转换为double,那么auto会被推断为double.

显示指定

1
2
3
4
5
6
7
8
9
10
#include <iostream>
template<typename RT, typename T1, typename T2>
RT max(T1 a, T2 b){
return a > b ? a : b;
}
int main() {
int a = 2;
double b = 3.14;
std::cout << ::max<double>(a, b) << std::endl;
}

type trait 类型萃取

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include<type_traits>
template<typename T1, typename T2, typename RT = std::common_type_t<T1,T2>>
RT max(T1 a, T2 b){
return a > b ? a : b;
}
int main() {
int a = 2;
double b = 3.14;
std::cout << ::max<double>(a, b) << std::endl;
}

std::common_type_t<>会返回多个类型的公共类型。例如std::common_type_t<int,double>等于double,因为可以隐式转换intdouble从而统一类型。

Concepts

在模板下添加requires

1
2
3
4
5
6
7
#include<concepts>

template <typename T>
requires std::totally_ordered<T> // 要求类型T可排序
T my_max(T a, T b) {
return a > b ? a : b;
}

可以尝试去掉以下代码中的requires,看看报错信息有多么令人头疼。加上requires,报错信息更精确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include<vector>
#include<string>
#include <concepts>

class Person{
private:
std::string name;
public:
template<typename STR>
requires std::is_convertible_v<STR,std::string>

explicit Person(STR&& n): name(std::forward<STR>(n)){
std::cout << "TMPL-CONSTR for " << name << "\n";
}
};
int main(){
Person p(123);
}

常见的约束:

概念(Concept) 用途 示例
std::integral<T> T 必须是整数类型(int, short 等) requires std::integral<T>
std::floating_point<T> T 必须是浮点类型(float, double requires std::floating_point<T>
std::copyable<T> T 可拷贝(有拷贝构造函数和赋值运算符) requires std::copyable<T>
std::movable<T> T 可移动(有移动构造函数和赋值运算符) requires std::movable<T>
std::equality_comparable<T> T 支持 ==!= requires std::equality_comparable<T>
std::totally_ordered<T> T 支持 <, <=, >, >= requires std::totally_ordered<T>
std::ranges::range<T> T 是范围(有 begin()end() requires std::ranges::range<T>
std::input_iterator<T> T 是输入迭代器 requires std::input_iterator<T>
std::invocable<F, Args...> F 可调用,参数为 Args... requires std::invocable<F, int, double>

Defining Concepts

检查成员函数是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<concepts>
#include<vector>
#include<string>
#include<iostream>
template <typename T>
requires requires(T obj){
obj.size();
obj.clear();
}
void reset(T& obj){
obj.clear();
}

int main(){
std::vector<int> v1{1,2};
reset(v1);
std::cout << v1.size() << std::endl;

int a = 1;
reset(a);//constraints not satisfied
return 0;
}

检查运算符

1
2
3
4
5
6
7
template<typename T>
requires requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // 必须支持 + 运算且返回 T
}
T add(T a, T b) {
return a + b;
}

Alias Template

1
2
3
4
5
6
7
8
9
template <typename T>
class MyContainer{
......
}

template<typename T>
using MyIterator = typename MyContainer<T>::iterator;//需要指明typename

MyIterator<int> pos;

Nontype Template Parameters

模板参数不一定是type,也可以是constant integral values

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
46
47
48
49
50
#include<iostream>
#include<array>
#include<cassert>

template <typename T, int MaxSize>
class myStack{
private:
std::array<T,MaxSize> elem;
int num;

public:
myStack();

void push(const T& _elem);

void pop();

T top();

int size();


};

template<typename T, int MaxSize>
myStack<T,MaxSize>::myStack(): num(0){}

template<typename T, int MaxSize>
void myStack<T,MaxSize>::push(const T& _elem){
assert(num < MaxSize);
elem[num++] = _elem;
}


template<typename T, int MaxSize>
void myStack<T, MaxSize>::pop(){
assert(num>0);
--num;
}

template<typename T, int MaxSize>
int myStack<T, MaxSize>::size(){
return num;
}

template<typename T, int MaxSize>
T myStack<T, MaxSize>::top(){
assert(num > 0);
return elem[num-1];
}

Variadic Templates

通过variadic Templates实现可变参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>

void print(){}

template <typename T, typename... Types>
void print(T first, Types... args){

std::cout << first << std::endl;
print(args...);
}

int main(){
print(1, 2.0, "Hello");
}

注意:要在模板前定义一个终止函数void print(){},否则模板展开时会反复调用print(arg...)导致爆栈。

实现and_all:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>


bool and_all(){
return true;
}

template<typename T>
bool and_all(T one){
return one;
}

template <typename T, typename... Types>
bool and_all(T first, Types... args){
return first && and_all(args...);
}

int main(){
std::cout << and_all(1,1,1,1,1,0) << std::endl;
std::cout << and_all(1) << std::endl;
}

Fold Expressions

折叠类型 语法 展开示例(参数包为 a, b, c
一元左折叠 ( ... op args ) ((a op b) op c)
一元右折叠 ( args op ... ) (a op (b op c))
二元左折叠 ( init op ... op args ) (((init op a) op b) op c)
二元右折叠 ( args op ... op init ) (a op (b op (c op init)))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>

template <typename... Types>
bool and_all(Types... args){
return (args && ...);
}

template<typename... Types>
auto add_all(Types... args){
return (0 + ... + args);// 而且括号不能少,不能写成0 + ... + args
}
int main(){
std::cout << and_all(1,1,1,1,1,0) << std::endl;
std::cout << add_all(1, 3.14, 2u) << std::endl;
}
  • 不能写成 return (args + ...); 防止无参数调用,例如add_all()

  • 括号不能少,不能写成return 0 + ... + args

  • (0 + ... + args) 就是( init op ... op args ) 的形式

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>

template <typename... Types>
void print_all(Types... args){

( (std::cout << args << "\n"), ... );
}
int main(){
print_all(1, "hello", 3.14, "world");

}
1
2
3
4
1
hello
3.14
world
  • ( (std::cout << args << "\n"), ... ) 是一元右折叠表达式.

args拼成了一个新的new_args = (std::cout << args << "\n")new_args中有四个变量,分别是(std::cout << 1 << "\n"), (std::cout << "hello" << "\n"), (std::cout << 3.14 << "\n"), (std::cout << "world" << "\n")

  • (new_args , ...) 一元右折叠,op = ,
1
2
3
4
5
6
7
8
9
10
11
#include<iostream>

template <typename T1, typename... TN>
constexpr bool isHomogeneous(T1, TN...){//可以省略参数名
return (std::is_same<T1,TN>::value && ...);
}
int main(){
std::cout << isHomogeneous(1, 2 , "hello") << std::endl;

std::cout << isHomogeneous(1, 2 , 3) << std::endl;
}

Generic Template Parameter & Reference Collasing & Perfect Forwarding

add_all这样写更好:

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>

template <typename... Args>
auto add_all(Args&&... args){
return (0 + ... + std::forward<Args>(args));
}

int main(){
int x = 6;
std::cout << add_all(1, x, 2, 3.14, 'a') << std::endl;
}
  • Generic Template Parameter:使用Args&&...能够同时处理lvaluervalue
  • Reference Collasing: int&&&等价于int&, int&&&& -> int&&
  • Perfect Forwarding: 完美转发,不改变左值右值属性

add_all被传入rvalue例如3.14时,Args推断为float, 完美转发后为float&& 3.14

add_all被传入lvalue例如x时,Args推断为int&, 完美转发后为int&&& x = int& x

一个使用完美转发前后的性能对比实验:link

typename keyword

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
#include<string>
#include<vector>
#include<tuple>
// print elements of an STL container

template<typename T>
void printcoll (T const& coll){
typename T::const_iterator pos;
typename T::const_iterator end(coll.end());// end position

for(pos = coll.begin(); pos != end; ++pos){
std::cout << *pos << std::endl;
}
}
int main(){
std::vector<int> v1{1, 2, 3};
std::vector<std::string> v2{"hello", "world"};

printcoll(v1);
printcoll(v2);
}

Disable Templates with enable_if_t<>

语法:

1
2
3
4
template <typename T>
std::enable_if_v< bool_expr, type > foo(){

}
  • 如果bool_expr为真,函数foo的返回值为type

  • 如果bool_expr为假,则foo不完成实例

  • type缺省为void

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <type_traits>

// 整数打印
template<typename T>
std::enable_if_t<std::is_integral_v<T>, void>
print(T value) {
std::cout << "Integer: " << value << std::endl;
}

// 浮点数打印
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, void>
print(T value) {
std::cout << "Floating: " << value << std::endl;
}

int main() {
print(42); // Integer
print(3.14f); // Floating
print("hello"); // no matching function
return 0;
}

可以使用默认模板参数+enable_if_t来对模板参数进行约束:

1
2
3
4
5
6
7
8
9
10
11
#include <type_traits>

template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void foo() {
std::cout << "T is integral" << std::endl;
}
int main() {
foo<int>(); // ✅ 编译通过
foo<double>(); // ❌ 编译失败(SFINAE 排除此版本)
return 0;
}
  • 第一个模板参数是T,第二个模板参数typename = std::enable_if_t<std::is_integral_v<T>> , 通过第二个模板参数的推导来限制第一个模板参数T的类型
  • 如果T是整数类型,模板展开为
1
tempalte<typename T, typename = void>

第二个模板参数用不上,只是为了去约束T的。

concept

个人觉得concept相较于enable_if更加简洁清晰。但是conceptC++20进入标准,可能老代码还是得用enable_if.

1
2
3
4
5
6
template<typename STR>
requires std::is_convertible_v<STR,std::string>

PerSon(STR&& n):name(std::forward<STR>(n)) {
......
}