闭关之 C++ Template 笔记(一):PartⅠ基本概念(一)
目录
- 前言
- 第一章 Function Templates
- 1.1 初识函数模板
- 1.1.1 定义模板
- 1.1.2 使用模板
- 1.1.3 二阶段翻译
- 1.2 模板参数推断
- 1.3 多模板参数
- 1.3.1 返回类型的模板参数
- 1.3.2 推断返回类型
- 1.3.3 作为通用类型返回
- 1.4 默认模板参数
- 1.5 重载函数模板
- 1.6 但是,难道我们不应该...?
- 1.6.1 按值传递还是按引用传递?
- 1.6.2 为什么不用 inline 呢?
- 1.6.3 为什么不用 constexpr 呢?
- 第二章 Class Template
- 2.1 类模板 Stack 的实现
- 2.1.1 类模板声明
- 2.1.2 成员函数的实现
- 2.2 类模板 Stack 的使用
- 2.3 类模板的局部使用
- 2.3.1 Concepts
- 2.4 友元
- 2.5 类模板特化
- 2.6 偏特化
- 2.7 默认类模板参数
- 2.8 类型别名
- 2.9 类模板参数推断
- 2.10 模板聚合
- 第三章 非类型模板参数
- 3.1 非类型类模板参数
- 3.2 非类型函数模板参数
- 3.3 非类型模板参数的限制
- 3.4 模板参数 auto
- 第四章 变参模板
- 4.1 变参模板
- 4.1.1 变参模板实例
- 4.1.2 重载变参和非变参模板
- 4.1.3 运算符 sizeof
- 4.2 折叠表达式
- 4.3 变参模板的应用
- 4.4 变参类模板和变参表达式
- 4.4.1 变参表达式
- 4.4.2 变参下标 (Variadic Indices)
- 4.4.3 变参类模板
- 4.4.4 变参推断指南 Deductions Guides
- 4.4.5 变参基类和 using
- 第五章 基础技巧
- 5.1 关键字 typename
- 5.2 零初始化
- 5.3 this->的使用
- 5.4 原始数组和字符串字面量模板
- 5.5 成员模板
- 5.5.1 构造 .template
- 5.5.2 泛型 Lambda 与成员模板
- 5.6 变量模板
- 5.7 模板的模板参数
前言
并发编程和函数式编程的书已经看完了。感觉就是,并发编程推荐使用函数式编程和 CSP,所以看了函数式编程相关的书籍;而 C++ 函数式编程又不得不依赖于模板和模板元编程。因此,买了一本《C++ Template 第二版》。已经开发这么多年了,模板已经不陌生了,但还是要再看一下,查缺补漏,温故知新。继续加油吧!
- 由于只有英文版,所以笔记可能有会有错误。毕竟英语是硬伤。
- 本来想买台版的,上次买了一本台版《Animation》就花了两个月。这次还是自己翻译吧!
- 官方资源
- 可以去卖书的平台下载
- 官方代码笔记放到这里: https://gitee.com/thebigapple/Study_CPlusPlus_20_For_CG.git
第一章 Function Templates
1.1 初识函数模板
1.1.1 定义模板
- 模板定义的语法
template< comma-separeted-list-of-parameters>
- 除了可以使用
typename
之外,还可以使用class
定义类型参数- 建议优先使用
typename
- 不可以使用
struct
- 建议优先使用
1.1.2 使用模板
- 作用域限定符
::
- 程序将会优先在全局作用域中查找函数模板
::max(f1, f2);
- 程序将会优先在全局作用域中查找函数模板
- 在编译阶段,模板会为程序每一个使用该模板的类型产生一个独立的函数实体
- 这个过程叫做模板实例化
- 模板实例化不需要手动请求,只需要使用函数模板就会触发实例化过程
void
也可以作为模板参数
1.1.3 二阶段翻译
- 在实例化模板的时候,如果模板的参数类型不支持使用到的操作符,将会遇到编译器错误
- 因为模板会被分两个步骤进行编译
- 模板定义阶段(未实例化),模板的检查包含以下几个方面
- 语法检查,例如缺少分号
- 使用了未定义的不依赖于模板参数的名称
- 未使用模板参数进行检查的静态断言( static assertions)
- 模板实例化阶段,为了确保所有代码的有效性,模板会再次被检查
- 所有依赖于模板参数的部分都会进行 double-checked
template<typename T> void foo(T t) {//如果 undeclared()未定义,第一阶段报错undeclared(); //如果 undeclared(t)未定义,第二阶段报错undeclared(t);static_assert(sizeof(int) > 10,"int too small"); static_assert(sizeof(T) > 10, "T too small"); }
- 模板定义阶段(未实例化),模板的检查包含以下几个方面
- 实际上,名称被检查两次称为 “two-phase lookup”
- 详情 14.3.1,Page 249
- 因为模板会被分两个步骤进行编译
- 有些编译器不会在第一阶段执行所有的检查。因此,如果模板没有被实例化过的话,可能不会发现模板代码中的问题
- 编译和连接
- 二阶段翻译的一个严重的问题
- 当实例化一个模板时,编译器需要知道模板的完整定义
- 第9章会讨论如何应对
- 暂时的做法:将模板的实现写在头文件里
- 二阶段翻译的一个严重的问题
1.2 模板参数推断
- 类型推断中的类型转换
- 类型推断过程中类型转换是受限制的
- 当调用参数为引用时,禁止任何形式的类型转换。通过相同模板类型参数 T 推断出的两个参数,必须严格匹配(两个参数类型必须一致)
- 当调用参数为值传递时,只支持 decay 转换
- const 和 volatile 会被忽略
- 引用被转换为被引用类型
- 这里所说的引用应该是常引用 (
int const&
)
- 这里所说的引用应该是常引用 (
- raw array 和 function 被转换成对应的指针类型
- 通过相同模板类型参数 T 推断出的两个参数,其退化(decay)后类型必须一致
- 类型推断过程中类型转换是受限制的
- 模板参数推断错误的几种解决办法 (这部分作为了解,实际开发中有很多制约手段)
- 强制类型转换
- 显式的提供参数类型,避免编译器自动推断
- 使用不同模板的参数
- 对默认形参的类型推断
- 类型推断不适用任何默认形参
- 如果要支持默认形参,需要为模板参数声明一个默认类型参数
template<typename T = std::string> void f(T = "");
1.3 多模板参数
- 当模板接受两个不同类型的模板参数,如果用其中一个模板参数类型作为返回值类型,这会导致一个问题
- 重复调用该函数时,由于函数实参类型的变化,返回值类型也会发生变化
- C++ 提供了不同方式应对这一问题
- 引入第三个模板参数作为返回值类型
- 让编译器推断返回值类型
- 将返回值类型定义为两个推断出参数类型的 “公共类型”
1.3.1 返回类型的模板参数
- 方法一
- 引入第三个模板参数作为返回值类型
- 然后,显式的指定三个模板参数的类型
- 问题
- 写法比较啰嗦
- 方法二
- 将返回值类型模板参数调整到模板定义第一位
- 然后,显式指定返回值模板参数类型,其它自动推断
- 问题
- 虽然只用显式指定一个模板参数类型,但还是需要显示指定
- 建议
- 使用单模板参数是比较好的选择
1.3.2 推断返回类型
- C++14 开始,如果返回值类型是由模板参数决定的,建议让编译器推断返回值类型
template<typename T1, typename T2> auto max (T1 a, T2 b) {return b < a ? a : b; }
- C++14 以前需要使用尾置返回类型
template<typename T1, typename T2> auto max (T1 a, T2 b) -> decltype(b<a?a:b) {return b < a ? a : b; }
decltype
会根据表达式结果确定返回值类型,这里可以简化,使用true
作为条件template<typename T1, typename T2> auto max (T1 a, T2 b) -> decltype(true?a:b) {return b < a ? a : b; }
- 如果
decltype
中的表达式返回值类型是引用类型,可以使用decay
#include <type_traits>template<typename T1, typename T2> auto max (T1 a, T2 b) -> typename std::decay<decltype(true? a:b)>::type {return b < a ? a : b; }
1.3.3 作为通用类型返回
- C++11 开始,标准库提供
std::common_type<>::type
#include <type_traits>template<typename T1, typename T2> std::common_type_t<T1,T2> max (T1 a, T2 b) {return b < a ? a : b; }
- C++11
typename std::common_type<T1,T2>::type
- C++14
std::common_type_t<T1,T2>
1.4 默认模板参数
#include <type_traits>template<typename T1,typename T2 = long, typename RT = std::decay_t<decltype(true ? T1() : T2())>>
RT max (T1 a, T2 b)
{return b < a ? a : b;
}
std::decay_t<>
可以确保返回的值不是引用类型。T1() T2()
要求两个模板参数必须有默认构造函数- 如果没有可以使用
std::declval
- 如果没有可以使用
- 也可以使用
std::common_type<>
作为返回值类型的默认值#include <type_traits>template<typename T1, typename T2, typename RT = std::common_type_t<T1,T2>> RT max (T1 a, T2 b) {return b < a ? a : b; }
std::common_type<>
也会做类型退化,因此返回值类型不会是引用
1.5 重载函数模板
- 非模板函数,可以和同名的函数模板共存,而且该同名的函数模板可以实例化出与非模板函数有相同形参和返回值类型。
- 调用原则
- 模板解析会优先选用非模板函数,而不是将模板函数实例化
- 如果模板可以实例化一个更匹配的函数,优选选择该模板
- 如果要强制使用模板实例化的函数进行调用,可以显示指定一个空模板列表
::max<>(7, 42);
- 推断模板参数不涉及自动类型转换,而常规函数实参可以根据形参类型自动转换
- 调用原则
- 关于字符串类型的模板参数推导可以详看Page 16
1.6 但是,难道我们不应该…?
1.6.1 按值传递还是按引用传递?
- 建议
- 除了简单类型 (如:基础类型和
std::string_view
) 外,其他建议按照引用传递 - 这样可以避免不必要的拷贝
- 除了简单类型 (如:基础类型和
- 按值传递的优势
- 语法简单
- 编译器能更好的进行优化
- 移动语义会使拷贝成本较低
- 某些情况下可能没有拷贝或移动
- 对使用模板的建议
- 模板形参即可能是简单类型,也可能是复杂类型,如果使用按引用传递,对简单类型不利
- 可以使用
std::ref()
和std::cref()
强制按引用传递参数 - 虽然按值传递字符串字面量 和 raw array 会遇到问题,但按引用传递,问题更严重
- 后期会讨论
1.6.2 为什么不用 inline 呢?
- 建议
- 函数模板不需要声明成 inline
- 例外
- 全特化模板时,由于其已不是“泛型”。因此,根据情况可以声明为 inline
- inline 只表明在程序中,函数的定义可能在程序中多次出现
- inline 会给编译器建议
- 在调用该函数的地方该函数应该被展开
- 在某些情况下可以提高效率
- 但也可能降低效率
- 在调用该函数的地方该函数应该被展开
- inline 会给编译器建议
- 现代编译器在没有 inline 关键字的情况下,也可以很好的决定是否将函数展开
- 但编译器依然会将 inline 纳入是否展开函数的考虑因素中
1.6.3 为什么不用 constexpr 呢?
- C++11 支持 constexpr 在编译期进行求值
template<typename T1, typename T2> constexpr auto max (T1 a, T2 b) {return b < a ? a : b; } ... int a[::max(sizeof(char), 1000u)];
第二章 Class Template
- Code_2_1_1
2.1 类模板 Stack 的实现
2.1.1 类模板声明
- 声明
template<typename T> class Stack {...};
- 用关键字 class 声明
template<class T> class Stack {...};
- 在类模板内部,T 可以像普通类型一样,用于声明成员变量和成员函数
- 除了在该模板类内部使用外,其他位置使用类模板定义类型都要使用
Stack<T>
- 构造函数示例
- 在类内部定义的两种方式
class Stack { …Stack (Stack const&); // copy constructorStack& operator= (Stack const&); // assignment operator… };
class Stack { …Stack (Stack<T> const&); // copy constructorStack<T>& operator= (Stack<T> const&); // assignment operator… };
- 一般使用 为了特殊处理,所以建议使用第一种方式定义
- 在类内部定义的两种方式
- 在类外使用
template<typename T> bool operator== (Stack<T> const& lhs, Stack<T> const& rhs);
- 与非模板类不同,不能在函数内部或块作用域内定义类模板
- 只能在全局作用域、命名空间、其他类的内部定义类模板
2.1.2 成员函数的实现
- 类内部实现
template<typename T> class Stack {void push (T const& elem) {elems.push_back(elem); // append copy of passed elem} };
- 类外部实现
template<typename T> void Stack<T>::push (T const& elem) {elems.push_back(elem); // append copy of passed elem }
2.2 类模板 Stack 的使用
- 在 C++17 以前需要显式指定模板参数类型
Stack< int> intStack;
- C++17 如果能从构造函数推断类型,无须指定
Stack intStack{2};
- 只有在模板函数和模板成员函数被调用时才会被实例化
- 如果类模板有静态成员,每个使用该类模板的类型都会实例化其相应的静态成员一次
- 类模板的模板参数可以是任意类型
- 唯一要求
- 使用的类型必须支持该模板类中对模板参数的各种操作
- 唯一要求
2.3 类模板的局部使用
- 在类模板中,模板参数不必作用所有成员函数或成员变量
- 但会出现一个问题
- 如果类模板的非模板成员函数使用模板参数时,遇到实例化参数不支持的操作时,会导致错误
- 该错误只能在调用该成员函数时被发现
- 如果类模板的非模板成员函数使用模板参数时,遇到实例化参数不支持的操作时,会导致错误
2.3.1 Concepts
- Concepts
- 表示一组反复被模板库要求的限制条件
- 如标准库的 random access iterator 和 default constructible
- C++11 开始可以使用 static_assert 和一些预定义 type traits 做一些简单的检查
- C++20 可以使用 Concepts 做复杂情况的检查
2.4 友元
- 这里使用一个
operator<<
的非成员函数实现介绍友元 - 需求
- 比通过
printOn()
来打印stack
的内容更好的办法是重载stack
的operator<<
- 使用非成员函数的
operator<<
,并在实现中调用printOn()
- 非模板函数
friend std::ostream& operator<< (std::ostream& strm, Stack<T>const& s) {s.printOn(strm);return strm; }
- 比通过
- 如果要先声明友元函数,再定义就比较复杂
- 有两种解决方式
- 将该友元函数声明为一个函数模板, 再定义
template<typename T> class Stack {template<typename U>friend std::ostream& operator<< (std::ostream&, Stack<U> const&); };
- 先前置声明一个输出
Stack<T>
的operator<<
模板, 再声明友元函数
//声明 Stack 的 operator<< 前先声明 Stack<T> template<typename T> class Stack; // Stack 的 operator<< template<typename T> std::ostream& operator<< (std::ostream&, Stack<T> const&); template<typename T> class Stack {// 友元 operator<< 的声明// 注意:// operator<< <T> 相当于声明了一个特化之后的非成员函数模板// 如果没有 <T> 代表定义了一个新的非模板函数friend std::ostream& operator<< <T> (std::ostream&, Stack<T> const&); };
- 有两种解决方式
2.5 类模板特化
- 与函数模板特化类似
- 注意
- 特化类模板时,建议特化所有成员函数
- 被特化的类模板
- 将类模板参数替换成特化的类型即可
2.6 偏特化
- 类模板可以被偏特化。从而为某些特殊情况提供特殊的实现
- 如偏特化一个
Stack<>
专门处理指针template<typename T> class Stack<T*> {private:std::vector<T*> elems; public:void push(T*); T* pop(); T* top() const; bool empty() const ; };
- 多模板参数的偏特化
// partial specialization: both template parameters have same type template<typename T> class MyClass<T, T> { … }; // partial specialization: second type is int template<typename T> class MyClass<T, int> { … }; // partial specialization: both template parameters are pointer typestemplate<typename T1, typename T2> class MyClass<T1*, T2*> { … };
2.7 默认类模板参数
- 与函数模板类似
template<typename T, typename Cont = std::vector<T>> class Stack {...};
2.8 类型别名
- Typedefs and Alias Declarations
- 有两种方法定义别名
- 使用关键字 typedef
typedef Stack<int> IntStack;
- 使用关键字 using (C++11)
using IntStack = Stack<int>;
- 使用关键字 typedef
- 建议使用 using
- 定义新名字的方式称为 type alias declaration。
- 新名字称为 type alias
- 有两种方法定义别名
- Alias Templates
- C++11 开始支持别名模板
template<typename T> using DequeStack = Stack<T, std::deque<T>>;
- C++11 开始支持别名模板
- Alias Templates for Member Types
- 为类模板的成员类型定义一种快捷方式
- 有如下定义
template<typename T> struct MyType {typedef … iterator;… };
template<typename T> struct MyType {using iterator = …;… };
- 可以进行如下定义
template<typename T> using MyTypeIterator = typename MyType<T>::iterator;
- 可以使用
MyTypeIterator< int> pos;
- 代替
typename MyType<T>::iterator pos;
- Type Traits Suffix_t
- C++14 标准库所有返回一个类型的type trait 定义了快捷方式
- C++11
typename std::add_const<T>::type
- C++14
std::add_const_t<T>
- C++11
- C++14 标准库所有返回一个类型的type trait 定义了快捷方式
2.9 类模板参数推断
- C++17 以前必须显式指定模板参数类型
- C++17 如果能从构造函数推断类型,无须指定
template<typename T> class Stack {private:std::vector<T> elems; // elements public:Stack () = default;Stack (T const& elem) // initialize stack with one element: elems({elem}) {}… };
- 声明示例
- C++17 以前
Stack<int> intStack;
- C++17
Stack intStack = 0;
- C++17 以前
- 字符串字面量的类模板参数推断
- C++17
Stack stringStack = "bottom"; // Stack<char const[7]> deduced since C++17
- 问题
- 当参数是按照 T 的引用传递时,参数类型不会被退化,上述形式被转化为
Stack< char const[7]>
- 这样就不能添加不同维度的字符串字面量了
- 当按值传递时,参数类型会被退化成
char const *
- 当参数是按照 T 的引用传递时,参数类型不会被退化,上述形式被转化为
- 改进
- 构造函数改成按值传递参数
- C++17
- Deduction Guides
- 上面的例子虽然按值传递,但其为指针,为了避免处理指针可以使用 deduction guides
Stack( char const*) -> Stack<std::string>;
- 指引语句必须出现在该类模板定义相同的作用域或命名空间内。
- 通常它紧跟着类模板的定义
->
后面的类型被称为推断指引的 “guided type”
2.10 模板聚合
- 聚合类
- class 或者 struct
- 没有用户定义的显式的,或继承的构造函数
- 没有 private 或者 protected 的非静态成员,
- 没有虚函数
- 没有 virtual,private 或者protected的基类
- 也可以是模板
- class 或者 struct
- 示例
template<typename T> struct ValueWithComment {T value;std::string comment; };
- 使用
ValueWithComment< int> vc; vc.value = 42; vc.comment = "initial value";
- C++17 聚合类的类模板可以使用 deduction guides
ValueWithComment(char const*, char const*) -> ValueWithComment<std::string>; ValueWithComment vc2 = {"hello", "initial value"};
std::array<>
是一个聚合类
第三章 非类型模板参数
3.1 非类型类模板参数
- 定义固定大小容器
- 优势
- 可以避免开发人员或标准库管理内存
- 缺点
- 具体大小很难确定
- 改进
- 让用户根据各自的情况指定容器大小
- 类模板使用非类型参数
- 优势
- 非类型参数
- 可以为常规数值
- 非类型的模板参数
- 不再是类型,而是某个值
- Code_3_1_1
- 非类型的模板参数示例
- 定义
template<typename T, std::size_t Maxsize> class Stack {private:std::array<T,Maxsize> elems; };
- 使用
Stack<int,20> int20Stack;
- 定义
- 可以指定默认值
template<typename T = int, std::size_t Maxsize = 100> class Stack { … };
- 不建议这样做
- 最好同时显式地指定两个模板参数
- 不建议这样做
3.2 非类型函数模板参数
- 与非类型类模板参数定义类似
template<int Val, typename T> T addValue (T x) {return x + Val; }
- 根据非类型模板参数推断返回值类型
template<auto Val, typename T = decltype(Val)> T foo();
- 保证传入的非类型模板参数的类型与模板参数类型一致
template<typename T, T Val = T{}> T bar();
3.3 非类型模板参数的限制
- 可以是
- 整型(包括枚举类型)
- 指向 objects/functions/members 的指针
- objects 或者 functions 的左值引用
- std::nullptr_t
- 不可以是
- 浮点型数值
- 类
- 当传递对象的指针或者引用作为模板参数时,对象不能是
- 字符串字面量
- 临时变量
- 数据成员
- 其它子对象
- 针对 C++ 不同版本的限制
- C++11,对象必须要有外部链接。
- C++14,对象必须是外部链接或者内部链接
- C++17,对象没有链接属性也是有效的
- 避免无效表达式
- 非类型模板参数可以是任何编译期表达式
C<sizeof(int) + 4, sizeof(int) == 4> c;
- 如果表达式中使用了
operator >
注意其与<>
冲突- 错误
C<42, sizeof(int) > 4> c;
- 改正
C<42, (sizeof(int) > 4)> c;
- 错误
- 非类型模板参数可以是任何编译期表达式
3.4 模板参数 auto
- C++17 可以不指定非类型模板参数的具体类型
- 使用 auto
template<typename T, auto Maxsize>
- 如果要确定 auto 的类型可以使用
using size_type = decltype(Maxsize);
- C++14 也可以使用 auto,让编译器推断出具体的返回值类型
auto size() const { return numElems; }
- 将字符串作为常量数组用于非类型模板参数有如下用法
#include <iostream>template<auto T> // take value of any possible nontype parameter (since C++17) class Message {public:void print() {std::cout << T << '\n';} };...Message<42> msg1; msg1.print(); // initialize with int 42 and print that value static char const s[] = "hello"; Message<s> msg2; // initialize with char const[6] "hello" msg2.print(); // and print that value
第四章 变参模板
4.1 变参模板
4.1.1 变参模板实例
#include <iostream>void print () {}
template<typename T, typename… Types>
void print (T firstArg, Types… args)
{std::cout << firstArg << '\n'; //print first argument//递归输出参数包剩余的参数print(args…); // call print() for remaining arguments
}
4.1.2 重载变参和非变参模板
#include <iostream>template<typename T>
void print (T arg)
{std::cout << arg << '\n'; //print passed argument
}template<typename T, typename… Types>
void print (T firstArg, Types… args)
{print(firstArg); // call print() for the first argumentprint(args…); // call print() for remainingarguments
}
- 调用
print()
的重载时会优先选择没有参数包的函数模板
4.1.3 运算符 sizeof
- C++11 引入
sizeof...
- 一个参数包包含元素的数量
template<typename T, typename… Types> void print (T firstArg, Types… args) {std::cout << firstArg << '\n'; //print first argumentstd::cout << sizeof…(Types) << '\n'; //print number of remainingtypesstd::cout << sizeof…(args) << '\n'; //print number of remainingargs… }
- 一个参数包包含元素的数量
4.2 折叠表达式
- C++17 提供了一个特性
- 使用 binary operator 计算所有参数包(可选初始值)参数的运算结果
- 如:计算参数包所有参数的和
template<typename … T> auto foldSum (T… s) {return (… + s); // ((s1 + s2) + s3) … }
- 如果参数包为空,上述表达式不合法
- C++17 可以折叠的表达式
- ( … op pack )
- ((( pack1 op pack2 ) op pack3 ) … op packN )
- ( pack op … )
- ( pack1 op ( … ( packN-1 op packN )))
- ( init op … op pack )
- ((( init op pack1 ) op pack2 ) … op packN )
- ( pack op … op init )
- ( pack1 op ( … ( packN op init )))
- ( … op pack )
- 几乎所有的 binary operator 都可以用于折叠表达式
template<typename T, typename… TP> Node* traverse (T np, TP… paths) {return (np ->* … ->* paths); // np ->* paths1 ->* paths2 … }
4.3 变参模板的应用
- 应用之一就是转发任意类型和数量的参数
- 通常使用移动语义对参数进行完美转发(perfectly forwarded)
namespace std {template<typename T, typename… Args> shared_ptr<T>make_shared(Args&&… args);class thread {public:template<typename F, typename… Args>explicit thread(F&& f, Args&&… args); …};template<typename T, typename Allocator = allocator<T>>class vector {public:template<typename… Args>reference emplace_back(Args&&… args);…}; }
- 常规模板参数的规则同样适用于变参模板参数
- 参数是按值传递,其参数会被拷贝,类型也会退化
template<typename… Args> void foo (Args… args);
- 参数是按引用传递,其参数会是实参的引用,且类型不会退化
template<typename… Args> void bar (Args const&… args);
- 参数是按值传递,其参数会被拷贝,类型也会退化
- 通常使用移动语义对参数进行完美转发(perfectly forwarded)
4.4 变参类模板和变参表达式
- 参数包还可以应用于其他地方
- 表达式
- 类模板
- using 声明
- deduction guides
4.4.1 变参表达式
- 输出参数包中的每个参数的倍数
template<typename… T> void printDoubled (T const&… args) {print (args + args…); }
- 参数包中的每个参数加 1
template<typename… T> void addOne (T const&… args) {print (args + 1…); // ERROR: 1… is a literal with too many decimalpointsprint (args + 1 …); // OKprint ((args + 1)…); // OK }
- 编译阶段的表达式也可以使用模板参数包
template<typename T1, typename… TN> constexpr bool isHomogeneous (T1, TN…) {return (std::is_same<T1,TN>::value && …); // since C++17 }
4.4.2 变参下标 (Variadic Indices)
- 函数通过变参下标(参数包)访问某参数的相应元素
template<typename C, typename… Idx> void printElems (C const& coll, Idx… idx) {print (coll[idx]…); }
- 调用
printElems(coll,2,0,3);
- 等价于
print (coll[2], coll[0], coll[3]);
- 调用
- 可以将非类型模板参数声明成参数包
template<std::size_t… Idx, typename C> void printIdx (C const& coll) {print(coll[Idx]…); }
- 调用
printIdx<2,0,3>(coll);
- 调用
4.4.3 变参类模板
- Tuple 的实现
template<typename… Elements> class Tuple;Tuple<int, std::string, char> t; // t can hold integer, string, andcharacter
- Variant 的实现
template<typename… Types> class Variant;Variant<int, std::string, char> v; // v can hold integer, string,orcharacter
- 定义一个表示一组下表的类
// type for arbitrary number of indices: template<std::size_t…> struct Indices {};
- 示例:打印
std::array
或者std::tuple
中元素的函数template<typename T, std::size_t… Idx> void printByIdx(T t, Indices<Idx…>) {print(std::get<Idx>(t)…); }
- 使用
std::array<std::string, 5> arr = {"Hello", "my", "new", "!", "World"}; printByIdx(arr, Indices<0, 4, 3>());
- 使用
auto t = std::make_tuple(12, "monkeys", 2.0); printByIdx(t, Indices<0, 1, 2>());
- 示例:打印
4.4.4 变参推断指南 Deductions Guides
std::array
的 Deductions Guidenamespace std {template<typename T, typename… U> array(T, U…) -> array<enable_if_t<(is_same_v<T, U> && …), T>, (1 + sizeof…(U))>; }
- 初始化
std::array
std::array a{42,45,77};
enable_if_t()
- 如果是 false 类型推断失败
- 这种方式可以确保所有元素是同一类型
4.4.5 变参基类和 using
- 示例
#include <string> #include <unordered_set>class Customer {private:std::string name; public:Customer(std::string const& n) : name(n) { }std::string getName() const { return name; } };//比较 Customer 对象 struct CustomerEq {bool operator() (Customer const& c1, Customer const& c2) const{return c1.getName() == c2.getName();} };//计算对象hash值 struct CustomerHash {std::size_t operator() (Customer const& c) const{return std::hash<std::string>()(c.getName());} };//从个数不定的基类派生出了一个新的类 //绑定每个基类中 operator() 的声明 // define class that combines operator() for variadic baseclasses: template<typename… Bases> struct Overloader : Bases… {using Bases::operator()…; // OK since C++17 }; int main() {// combine hasher and equality for customers in one type:using CustomerOP = Overloader<CustomerHash,CustomerEq>;std::unordered_set<Customer,CustomerHash,CustomerEq> coll1;std::unordered_set<Customer,CustomerOP,CustomerOP> coll2; … }
第五章 基础技巧
5.1 关键字 typename
- typename
- 阐明模板中的一个标识符代表的是某种类型
template<typename T> class MyClass {public: …void foo() {typename T::SubType* ptr;} };
- 第二个
typename
被用来阐明SubType
是定义在T
中的一个类型ptr
是一个指向T::SubType
类型的指针
- 如果没有
typename
的话,SubType
会被假设成一个非类型成员- 如静态成员或枚举
- 阐明模板中的一个标识符代表的是某种类型
- 当一个依赖于模板参数的名称代表的是某种类型的时候,就必须使用 typename
5.2 零初始化
- 对于基础类型,如 int,double 及指针类型,由于它们没有默认构造函数,因此它们不会被默认初始化,需要使用 0 显式初始化
- 在定义模板时,如果想让一个模板类型的变量被初始化成一个默认值,需要考虑无默认构造的数据类型
- 使用 value initialization 方法
T x{};
- C++11 之前
T x = T();
- C++17 之前,只有在与拷贝初始化对应的构造函数没有被声明为 explicit 的时候,这一方式才有效
- C++17 开始,由于强制拷贝省略(mandatory copy elision)的使用,这一限制被解除,因此以上两种方式都有效。
- 使用 value initialization 方法
- 花括号初始化
- 如果没有可用的默认构造函数,还可以使用 initializer-list constructor
T x{1, 2};
- 默认的构造函数对相应成员做初始化
- C++11 之后
MyClass() : x{} {}
- C++11 之前
MyClass() : x() {}
- C++11 之后
- C++11 之后可以通过如下方式对非静态成员进行默认初始化
T x{};
- 不能对函数的默认参数使用这一方式
void foo(T p{}) {} // error
- 改用
void foo(T p = T{})
5.3 this->的使用
- 当使用定义在基类中的、依赖于模板参数的成员时,用
this->
或Base<T>::
来修饰调用函数template<typename T> class Base {public:void bar(); };template<typename T> class Derived : Base<T> {public:void foo() {bar(); // calls external bar() or errorBase<T>::bar();} };
5.4 原始数组和字符串字面量模板
- 当向模板传递原始数组和或者字面量时需注意
- 如果参数是按引用传递的,那么参数类型不会退化
- 如
"hello"
会被推断为char const[6]
- 如
- 如果参数是按值传递的,那么参数类型会退化
- 字符串字面量会被退化成
char const *
- 字符串字面量会被退化成
- 如果参数是按引用传递的,那么参数类型不会退化
- 处理原始数组或字符串字面量的模板示例
template<typename T, int N, int M> bool less (T(&a)[N], T(&b)[M]) {for (int i = 0; i<N && i<M; ++i){if (a[i]<b[i]) return true; if (b[i]<a[i]) return false;}return N < M; } ...int x[] = {1, 2, 3}; int y[] = {1, 2, 3, 4, 5}; //T会被实例化成 int,N 被实例化成 3,M 被实例化成 5 std::cout << less(x,y) << '\n'; std::cout << less("ab","abc") << '\n';
- 用来处理字符串字面量的函数模板示例
template<int N, int M> bool less (char const(&a)[N], char const(&b)[M]) {for (int i = 0; i<N && i<M; ++i) {if (a[i]<b[i]) return true;if (b[i]<a[i]) return false;}return N < M; }
- 对处理数组的函数模板的所有可能的重载示例
- Code_5_4_1
- 对特殊情况做的特化
- 边界已知和未知的数组
- 边界已知和未知的数组的引用
- 指针
- 针对未知边界数组定义的模板,可以用于不完整类型
- 如
extern int i[];
- 当按照引用传递时,它的类型是
int(&)[]
- 如
5.5 成员模板
- 类的成员也可以是模板
- 如嵌套类和成员函数
- 设有一个 stack 模板,通常只有当两个实例化的模板类型相同时才可以相互赋值
- 如果两个实例化的参数类型可以隐式转换也不能相互赋值
Stack<int> intStack1, intStack2; // stacks for ints Stack<float> floatStack; // stack for floats intStack1 = intStack2; // OK: stacks have same type floatStack = intStack1; // ERROR: stacks have different types
- 为了实现可以对模板实例化的不同参数类型实例相互赋值,可以将赋值运算符定义成模板
- Code_5_5_1
- 在模板类型为 T 的模板内部,定义一个模板类型为T2 的内部模板语法
template<typename T> template<typename T2> …
- 为了访问 op2 的成员,可以将其它所有类型的 stack 模板的实例都定义成友元
template<typename> friend class Stack;
- Code_5_5_1 如果将 string 类型的 stack 赋值给 int 类型的 stack 会报错
- 需要改变内部的容器类型
- Code_5_5_2 解决上述问题
- 其实就是将容器类型也设置为类模板的模板参数
- 需要改变内部的容器类型
- 特化成员模板
- 成员函数模板也可以被全部或者部分地特化
class BoolString {private:std::string value; public:BoolString (std::string const& s): value(s) {}template<typename T = std::string>T get() const // get value (converted to T){ return value;} };
- 成员函数模板进行全特化
//为避免重复定义的错误,必须将它定义成 inline 的。 template<> inline bool BoolString::get<bool>() const {return value == "true" || value == "1" || value == "on"; }
- 注意我们不需要也不能够对特化模板进行声明
- 使用
std::cout << std::boolalpha; BoolString s1("hello"); std::cout << s1.get() << ’\n’; //prints hello std::cout << s1.get<bool>() << ’\n’; //prints false BoolString s2("on"); std::cout << s2.get<bool>() << ’\n’; //prints true
- 成员函数模板也可以被全部或者部分地特化
- 特殊成员函数的模板
- 当特殊成员函数允许复制和移动对象,也可使用模板
- 与其他成员模板定义类似
- 构造函数也可以使用模板
- 注意
- 构造函数模板或者赋值运算符模板不会取代预定义的构造函数和赋值运算符
- 成员函数模板不会被算作用来复制和移动对象的特殊成员函数
- 相同类型的 stack 之间相互赋值,调用的依然是默认赋值运算符
- 优势和缺点
- 构造函数模板或赋值运算符模板可能比预定义的复制/移动构造函数或者赋值运算符更匹配
- 想要对复制/移动构造函数进行模板化并不是一件容易的事情
- 如何限制其存在
- 当特殊成员函数允许复制和移动对象,也可使用模板
5.5.1 构造 .template
- 有时,在调用成员模板的时候需要显式地指定其模板参数的类型。这时候就需要使用关键字 template 来确保
<
为模板参数列表的开始,而不是一个比较运算符template<unsigned long N> void printBitset (std::bitset<N> const& bs) {std::cout << bs.template to_string<char, std::char_traits<char>, std::allocator<char>>(); }
.template
标识符(->template 和 ::template 也类似)只能被用于模板内部- 并且它前面的对象依赖于模板参数时使用
5.5.2 泛型 Lambda 与成员模板
- C++14 引入的泛型 lambdas
- 是定义成员模板的快捷方式
- lambda 定义
[] (auto x, auto y) {return x + y; }
- 编译器构造
class SomeCompilerSpecificName {public:SomeCompilerSpecificName(); // constructor only callable bycompilertemplate<typename T1, typename T2>auto operator() (T1 x, T2 y) const {return x + y;} };
5.6 变量模板
- C++14开始,变量也可以被某种类型参数化。称为变量模板
template<typename T> constexpr T pi{3.1415926535897932385};
- 不能定义在函数内部或块作用域内
- 在使用变量模板的时候,必须指明它的类型
std::cout << pi<double> << '\n';
std::cout << pi<float> << '\n';
- 变量模板可以用于不同翻译单元
- 变量模板可有默认模板类型
template<typename T = long double> constexpr T pi = T{3.1415926535897932385}; ... std::cout << pi<> << '\n';
- 变量模板可以用非类型参数进行参数化,也可以用非类型参数进行变量模板的初始化
- 示例
#include <iostream> #include <array>template<int N> std::array<int,N> arr{}; // array with N elements, zero-initializedtemplate<auto N>constexpr decltype(N) dval = N; // type of dval depends on passed valueint main() {std::cout << dval<'c'> << '\n'; // N has value 'c' of type chararr<10>[0] = 42; // sets first element of global arrfor (std::size_t i=0; i<arr<10>.size(); ++i) // uses valuessetin arr{std::cout << arr<10>[i] << '\n';} }
- 示例
- 用于数据成员的变量模板
- 定义表示类模板成员
- 示例
template<typename T> class MyClass {public:static constexpr int max = 1000; };
- 可以为 MyClass<>的不同特化版本定义不同的值
template<typename T> int myMax = MyClass<T>::max;
- 使用
auto i = myMax<std::string>;
- 而不是
auto i = MyClass<std::string>::max;
- 标准库的类使用
- 示例
namespace std {template<typename T>class numeric_limits {public:…static constexpr bool is_signed = false; …}; }
- 可以定义
template<typename T> constexpr bool isSigned = std::numeric_limits<T>::is_signed;
- 使用
isSigned<char>
- 代替
std::numeric_limits<char>::is_signed
- 示例
- Type Traits “_v” 后缀
- C++17 标准库使用变量模板技术为所有的 type traits 产生一个值定义了简化方式
std::is_const_v<T> // since C++17
std::is_const<T>::value //since C++11
- 定义如下
namespace std {template<typename T>constexpr bool is_const_v = is_const<T>::value; }
- C++17 标准库使用变量模板技术为所有的 type traits 产生一个值定义了简化方式
5.7 模板的模板参数
- 允许模板参数是一个类模板,会很实用
- 使用 Code_5_5_2 的 stack 模板声明如下
Stack<int, std::vector<int>> vStack;
- 模板的模板参数,在声明 Stack 类模板的时候就可以只指定容器的类型而不去指定容器中元素的类型
Stack<int, std::vector> vStack;s
- 代码如下
template<typename T,//声明模板参数时可以使用 class 代替 typenametemplate<typename Elem> class Cont = std::deque> class Stack {private:Cont<T> elems; // elements public:void push(T const&); // push elementvoid pop(); // pop elementT const& top() const; // return top elementbool empty() const { // return whether the stack is emptyreturn elems.empty();} … };
- 与 Code_5_5_2 的 stack 相比,区别在于第二个模板参数被定义为一个类模板
- C++11 开始,也可以用别名模板(alias template)取代 Cont
- C++17 开始,在声明模板的模板参数时才可以用 typename 代替 class
template<typename T, template<typename Elem> typename Cont = std::deque> class Stack //ERROR before C++17 { … };
- 模板的模板参数中的模板参数没有被用到,作为惯例可以省略它
template<typename T, template<typename> class Cont = std::deque> class Stack { … };
- 成员函数更改
template<typename T, template<typename> class Cont> void Stack<T,Cont>::push (T const& elem) {elems.push_back(elem); // append copy of passed elem }
- 模板的模板参数匹配
- 如果使用上面版本的 Stack,可能会遇到错误
- 默认的
std::deque
和 模板模板参数 Cont 不匹配
- 默认的
- 原因
- 在 C++17 之前,模板模板参数必须和实际参数(std::deque)的模板参数匹配
std::deque
有两个参数,第二个是默认参数allocator
- 默认参数也要被匹配
- 在 C++17 之前,模板模板参数必须和实际参数(std::deque)的模板参数匹配
- 修正
template<typename T,template<typename Elem,typename Alloc = std::allocator<Elem>> class Cont = std::deque> class Stack {private:Cont<T> elems; // elements… };
Alloc
可以省略掉- C++17 可以不修正
- 如果使用上面版本的 Stack,可能会遇到错误
- 修正后完整的 stack 代码
- Code_5_7_1
闭关之 C++ Template 笔记(一):PartⅠ基本概念(一)相关推荐
- vue学习笔记-03-浅谈组件-概念,入门,如何用props给组件传值?
vue学习笔记-03-浅谈组件-概念,入门,如何用props给组件传值? 文章目录 vue学习笔记-03-浅谈组件-概念,入门,如何用props给组件传值? 什么是组件? 为什么要使用组件? 如何使用 ...
- tensorflow笔记:流程,概念和简单代码注释
tensorflow是google在2015年开源的深度学习框架,可以很方便的检验算法效果.这两天看了看官方的tutorial,极客学院的文档,以及综合tensorflow的源码,把自己的心得整理了一 ...
- MySQL学习笔记01【数据库概念、MySQL安装与使用】
MySQL 文档-黑马程序员(腾讯微云):https://share.weiyun.com/RaCdIwas 1-MySQL基础.pdf.2-MySQL约束与设计.pdf.3-MySQL多表查询与事务 ...
- CISAW风险管理学习笔记(2)-风险管理基本概念
个人学习总结,CISAW学习笔记之风险管理基本概念:
- 2.SQL SERVER笔记——SQL SERVER系统概念
2.SQL SERVER笔记--SQL SERVER系统概念 系统数据库 数据库管理员(DBA)的一项基本的技能是对SQL数据库引擎的系统数据库的深刻理解.数据库开发人员了解SQLSERVER自带的系 ...
- c语言注释语句执行吗,C语言学习笔记之C语言概念解析(附资料分享)每一个语句都必须以分号结尾但预处理命令函数头和花括号“}”之后不能加分号...
[[怪兽爱C语言]C语言学习笔记之C语言概念解析(附资料分享)]https://toutiao.com/group/6582429294901854728/?iid=15906422033&a ...
- QML笔记:QML基本概念及使用
QML笔记:QML基本概念及使用 Qt5中的Qt Qml和Qt Quick架构 Qt Qml模块本身并没有涉及图形显示,所有的图形处理都由Qt Quick模块完成. Qt Quick 以QPA(Qt ...
- 闭关之现代 C++ 笔记汇总(二):特性演化
目录 前言 C++98 C++98 之前 C++98 的主要语言特性 特性总结 dynamic_cast RAII 标准库组件 总结 find_if 其他语言对 C++ 影响 (非 C++98 内容 ...
- c++ template笔记(2)模板类
1.自定义Stack模板类 #include <vector> #include <stdexcept>template <typename T> class St ...
最新文章
- Linux网站架构系列之Apache----进阶篇
- 【C 语言】数组 ( 指针退化验证 | 计算数组大小 | #define LENGTH(array) (sizeof(array) / sizeof(*array)) )
- R语言--查看数据类型+类型判断
- 从一个工程师到管理员的经验分享
- 电脑技巧:键盘上最长的按键空格键使用技巧!
- java 开发注意项_JAVA开发注意事项集锦
- 函数 注释规范_C++掌握标准编码规范,摘掉初级“码农”帽子!
- mariadb安装密码验证插件
- 计算机软考中级网络工程师考点总结——待续
- css3揭秘读书笔记--边框内圆角
- 计算机系统机构中的八个伟大思想
- echarts 简单词云制作,自定义图案词云echarts-wordcloud.js
- 学习大数据开发零基础是不是限制,小白能否快速学会?附上学习路线图
- opencv ipcam
- vue——数字加逗号分隔
- 为什么很多人开始反对996了?
- 老旧小区为什么要进行安防升级改造
- 云开发系列课程 | FaaS场景下的SSR框架
- 计算机台式右上角三个灯作用,键盘上的三个灯分别代表什么意思 三个灯各有什么作用呢...
- 你知道嵌入式可以做什么工作吗?