本文是阅读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,因为可以隐式转换int为double从而统一类型。
Concepts 在模板下添加requires
1 2 3 4 5 6 7 #include <concepts> template <typename T>requires std::totally_ordered<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); 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 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;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); } 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" ); }
( (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&&...能够同时处理lvalue和rvalue
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 keyword1 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> template <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::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 () { }
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 ); print (3.14f ); print ("hello" ); 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 >(); 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更加简洁清晰。但是concept在C++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)) {...... }