函数模板与实例化
模板是什么
- 普通函数只能处理声明好的类型
- 如果还要处理
double、std::string,不使用模板就需要重复编写
- 如果还要处理
|
1 2 3 4 |
int max_value(int a, int b) { return b < a ? a : b; } |
- 模板的作用是
- 描述一组具有相同结构、但类型不同的函数
- 这段代码本身不是一个普通函数,而是一个函数模板,也可以理解为编译器生成函数的“配方”
|
1 2 3 4 5 |
template<typename T> T max_value(T a, T b) { return b < a ? a : b; } |
模板参数和函数参数
- 这里有两种完全不同的参数
|
1 2 |
template<typename T> T max_value(T a, T b); |
| 名称 | 示例 | 作用 |
| 模板参数 | T |
在编译期代表一种类型 |
| 函数参数 | a、b |
在运行期保存具体数值 |
typename T表示T是一个类型参数
- 在声明类型模板参数时,
class和typename含义相同T不要求一定是类,也可以是int等基础类型,因此我通常推荐typename
|
1 2 3 |
// 也可以写成 template<class T> |
示例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> #include <string> template<typename T> T max_value(T a, T b) { return b < a ? a : b; } int main() { std::cout << max_value(7, 42) << '\n'; std::cout << max_value(3.5, 2.1) << '\n'; std::string s1 = "asio"; std::string s2 = "template"; std::cout << max_value(s1, s2) << '\n'; } |
编译器遇到调用时做了什么
|
1 |
max_value(7, 42); |
- 第一步:发现函数模板
|
1 2 |
template<typename T> T max_value(T a, T b); |
- 第二步:推导
T
|
1 2 3 4 |
// 第一个实参是 `int` T → int // 第二个实参也是 int,推导结果一致 |
- 第三步:替换模板参数
max_value<int>称为函数模板的一个特化
|
1 2 3 4 |
int max_value<int>(int a, int b) { return b < a ? a : b; } |
- 第四步:实例化
- 当程序确实需要这个函数时,编译器根据模板生成相应定义
- 这个过程称为实例化
- 总结,需要区分:
- 模板:生成代码的规则
- 模板实参:例如
int - 特化:例如
max_value<int> - 实例化:根据模板产生对应特化的过程
显式指定模板实参
- 也可以直接告诉编译器
T是什么- 因为
T已经指定为double,7会转换为double
- 因为
|
1 |
double result = max_value<double>(7, 4.5); |
- 概念上调用的是
|
1 |
double max_value<double>(double, double); |
为什么混合类型会失败
- 下面通常无法通过编译
- 同一个
T得到两个冲突结果,推导失败
- 同一个
|
1 |
max_value(7, 4.5); |
|
1 2 3 4 |
// 推导过程为 从第一个参数推导:T = int 从第二个参数推导:T = double |
- 一个非常重要的规则是:
- 模板参数推导期间,编译器通常不会先进行任意隐式类型转换来帮助推导
模板并不真的支持“任意类型”
- 书中经常说
T可以代表任意类型。这句话容易误解 - 更准确地说:
T可以替换成任意类型,但替换之后形成的代码必须合法
- 当前模板使用了:
- 因此
T必须支持相应的<操作,并且结果能够用于条件判断
- 因此
|
1 |
b < a |
- 当前函数按值接收和返回对象
- 所以还涉及
T的构造、复制或移动能力
- 所以还涉及
|
1 |
T max_value(T a, T b); |
|
1 2 3 4 5 6 7 8 |
struct Packet {}; Packet p1; Packet p2; max_value(p1, p2); // 错误:Packet 没有可用的 operator< // 模板不会自动赋予 Packet 比较能力 |
- 在
C++20中,我们可以通过Concepts把这些要求直接写进接口
为什么模板通常写在头文件中
- 普通函数可以这样组织
|
1 2 |
max.hpp :只有声明 max.cpp :函数定义 |
- 但函数模板通常必须让使用它的编译单元看到完整定义,因为编译器需要根据具体的
T实例化代码- 所以常见结构是:
|
1 2 3 4 5 6 7 8 |
// max.hpp #pragma once template<typename T> T max_value(T a, T b) { return b < a ? a : b; } |
- 大型项目中也经常使用
|
1 2 3 4 5 |
// max.hpp template<typename T> T max_value(T a, T b); #include "max.tpp" |
练习
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> template <typename T> T clamp_value(T value, T low, T high) { if (value < low) { return low; } else if (value > high) { return high; } else { return value; } } int main() { auto c1 = clamp_value(15, 0, 10); // 返回 10 auto c2 = clamp_value(-2, 0, 10); // 返回 0 auto c3 = clamp_value(5, 0, 10); // 返回 5 return 0; } |
多个模板参数、返回类型推导与函数模板重载
单一模板参数的限制
|
1 2 3 4 5 |
template<typename T> T max_value(T a, T b) { return b < a ? a : b; } |
- 下面可以推导
|
1 2 |
max_value(10, 20); // T = int max_value(1.5, 2.5); // T = double |
- 下面会失败
|
1 |
max_value(10, 20.5); |
- 因为推导发生冲突
|
1 2 |
根据第一个参数:T = int 根据第二个参数:T = double |
多个模板参数
|
1 2 3 4 5 |
template<typename T1, typename T2> auto max_value(T1 a, T2 b) { return b < a ? a : b; } |
- 调用
|
1 |
auto result = max_value(10, 20.5); |
- 推导结果
|
1 2 |
T1 = int T2 = double |
- 返回表达式为
- 由于条件运算符需要为第二、第三个操作数寻找兼容类型,
int会转换为double,所以最终返回double
- 由于条件运算符需要为第二、第三个操作数寻找兼容类型,
|
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 T1, typename T2> constexpr auto max_value(T1 a, T2 b) { return b < a ? a : b; } int main() { auto a = max_value(10, 20); auto b = max_value(10, 20.5); auto c = max_value(10.5, 20); static_assert(std::is_same_v<decltype(a), int>); static_assert(std::is_same_v<decltype(b), double>); static_assert(std::is_same_v<decltype(c), double>); std::cout << a << '\n'; std::cout << b << '\n'; std::cout << c << '\n'; } |
|
1 2 3 4 |
// 这里的 constexpr 不是模板所必需的,只是让该函数也可以在编译期使用 constexpr auto value = max_value(10, 20); static_assert(value == 20); |
为什么不能直接返回 T1
- 它虽然能编译,但返回类型依赖参数顺序
|
1 2 3 4 5 |
template<typename T1, typename T2> T1 max_value(T1 a, T2 b) { return b < a ? a : b; } |
|
1 2 |
auto a = max_value(42, 66.6); // T1=int,结果为 int 66 auto b = max_value(66.6, 42); // T1=double,结果为 double 66.6 |
显式设置返回类型
- 一种方法是增加返回类型模板参数
|
1 2 3 4 5 |
template<typename RT, typename T1, typename T2> RT max_value(T1 a, T2 b) { return b < a ? a : b; } |
|
1 2 3 |
// 调用时只显式指定第一个模板参数 auto result = max_value<double>(10, 20.5); |
|
1 2 3 |
RT = double // 显式指定 T1 = int // 推导 T2 = double // 推导 |
- 模板参数可以只显式指定前面一部分,剩余参数继续推导
|
1 2 3 4 |
template<typename A, typename B, typename C> void f(B, C); f<int>(1.5, 'a'); |
|
1 2 3 |
A = int B = double C = char |
C++14 auto返回类型推导
- 从
C++14开始,可以让编译器根据return表达式推导返回类型- 这里的
auto和变量声明中的auto类似,通常会移除顶层引用和顶层const,返回一个值
- 这里的
|
1 2 3 4 5 |
template<typename T1, typename T2> auto max_value(T1 a, T2 b) { return b < a ? a : b; } |
- 需要注意:
- 如果函数中有多个
return,每个返回表达式必须推导出相同类型
- 如果函数中有多个
|
1 2 3 4 5 6 7 8 |
auto f(bool condition) { if (condition) { return 1; // int } return 2.5; // double,错误 } |
|
1 2 3 4 5 6 7 8 |
auto f(bool condition) { if (condition) { return 1.0; } return 2.5; } |
C++11 的尾置返回类型
C++11还不能直接根据函数体推导普通函数的auto返回类型,因此当时常写成:- 之所以使用尾置返回类型,是因为只有声明完
a、b后,才能在返回类型中使用它们
- 之所以使用尾置返回类型,是因为只有声明完
|
1 2 3 4 5 6 |
template<typename T1, typename T2> auto max_value(T1 a, T2 b) -> decltype(b < a ? a : b) { return b < a ? a : b; } |
- 这里存在一个严重陷阱
- 返回的引用会悬空,使用它会产生未定义行为
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 假设调用 max_value(10, 20); // 函数内部的 `a` 和 `b` 都是具名变量,所以都是左值 // 当 a、b 类型相同时,其结果也是左值 b < a ? a : b // 因此: 可能得到int& decltype(b < a ? a : b) // 这会导致函数返回局部变量的引用 template<typename T1, typename T2> auto bad_max(T1 a, T2 b) -> decltype(b < a ? a : b) { return b < a ? a : b; // 可能返回 a 或 b 的引用 } // a、b 在这里销毁 |
- 严格的
C++11写法为
|
1 2 3 4 5 6 7 8 |
#include <type_traits> template<typename T1, typename T2> auto max_value(T1 a, T2 b) -> typename std::decay<decltype(b < a ? a : b)>::type { return b < a ? a : b; } |
std::common_type
C++11标准库提供了std::common_type,用于寻找多个类型的公共类型
|
1 2 3 4 5 6 7 8 |
#include <type_traits> template<typename T1, typename T2> std::common_type_t<T1, T2> max_common(T1 a, T2 b) { return b < a ? a : b; } |
函数模板重载
- 函数模板可以和普通函数同名
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int max_value(int a, int b) { std::cout << "普通函数\n"; return b < a ? a : b; } template<typename T> T max_value(T a, T b) { std::cout << "函数模板\n"; return b < a ? a : b; } |
- 调用
- 两个候选都能精确匹配
- 当匹配质量相同时,普通函数优先
|
1 2 3 4 |
max_value(7, 42); int max_value(int, int); int max_value<int>(int, int); |
转换质量优先于“普通函数优先”
- 调用
|
1 2 3 4 5 6 |
max_value(7.0, 42.0); int max_value(int, int) //两次 double → int max_value<double> //精确匹配 // 模板版本匹配得更好,因此选择模板 |
- 所以规则不是“永远优先普通函数”,而是
- 先比较参数匹配质量;只有其他条件相同时,才优先普通函数
强制选择模板
- 可以使用空模板实参列表
- 这表示只考虑函数模板,并由实参推导
T
- 这表示只考虑函数模板,并由实参推导
|
1 2 3 |
max_value<>(7, 42); T = int |
- 也可以显式指定
|
1 2 3 4 |
max_value<double>(7, 42); // 两个 int 实参会转换为 double double max_value<double>(double, double); |
两个函数模板之间的选择
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 通用版本:允许两个不同类型 template<typename T1, typename T2> auto max_value(T1 a, T2 b) { return b < a ? a : b; } // 更特殊:要求两个参数具有相同类型 template<typename T> T max_value(T a, T b) { return b < a ? a : b; } |
- 调用
- 两个模板都匹配,但第二个要求两个参数必须具有相同类型,限制更严格,因此它更特化,编译器选择第二个
|
1 |
max_value(1, 2); |
- 第二个模板推导冲突,只有第一个可用
|
1 |
max_value(1, 2.5); |
返回类型不能单独构成重载
|
1 2 3 4 5 |
template<typename T> int convert(T value); template<typename T> double convert(T value); // 错误 |
- 调用
- 编译器不能根据接收返回值的位置选择函数
|
1 |
auto result = convert(10); |
- 因此
- 返回类型不参与普通的函数重载选择
- 如果需要不同返回类型,应
- 显式添加返回类型模板参数
- 使用不同函数名
- 增加能够参与重载的参数
- 使用
Tag Dispatch、Concepts或策略类型
一个容易忽略的危险调用
- 假设有
|
1 2 3 4 |
int max_value(int, int); template<typename T> T max_value(T, T); |
- 调用
|
1 2 3 4 5 6 7 8 9 10 11 12 |
max_value('a', 42.7); // 模板推导失败 第一个参数推导 T = char 第二个参数推导 T = double // 但普通函数仍然可用 char → int double → int // 所以它可能调用 // 42.7 被截断成 42 int max_value(int, int); |
- 这再次说明
- 模板推导失败不代表整个调用失败,编译器还会继续检查其他重载
类模板
概述
- 函数模板描述“一组函数”,类模板描述“一组类”
|
1 2 |
template<typename T> class Stack; |
- 它不是一个具体类型,而是生成类型的模板
|
1 2 3 |
Stack<int> // 一个具体类型 Stack<double> // 另一个具体类型 Stack<std::string> // 又一个具体类型 |
- 核心规则是
- 类模板本身不是具体类型;填入模板实参后形成的
Stack<int>才是类型
- 类模板本身不是具体类型;填入模板实参后形成的
最小类模板
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template<typename T> class Box { public: void set(const T& value) { value_ = value; } const T& get() const { return value_; } private: T value_{}; }; |
|
1 2 3 4 5 |
Box<int> int_box; int_box.set(42); Box<std::string> string_box; string_box.set("hello"); |
实现 Stack<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 32 33 34 35 36 |
#include <cstddef> #include <ostream> #include <stdexcept> #include <utility> #include <vector> template<typename T> class Stack { public: using value_type = T; using size_type = std::size_t; void push(const T& value); void push(T&& value); void pop(); T& top(); const T& top() const; [[nodiscard]] bool empty() const noexcept { return elements_.empty(); } [[nodiscard]] size_type size() const noexcept { return elements_.size(); } void print(std::ostream& out) const; private: std::vector<T> elements_; }; |
在类外定义成员函数
- 如果成员函数定义在类模板外面,需要重新写出模板声明
|
1 2 3 4 5 6 7 8 9 |
template<typename T> void Stack<T>::push(const T& value) { elements_.push_back(value); } // 需要同时写出两部分 template<typename T> // 说明下面定义依赖模板参数 T void Stack<T>::push // 说明这是 Stack<T> 的成员 |
- 为什么必须写
Stack<T>::
|
1 2 3 4 5 6 7 |
template<typename T> void Stack::push(const T& value) // 错误 { } // 在类模板外,Stack 表示模板名称,不是当前要定义的具体类类型。必须写: Stack<T>::push |
类内部可以省略 <T>
- 在类模板内部
|
1 2 3 4 5 6 7 8 9 |
template<typename T> class Stack { public: Stack(const Stack& other); Stack& operator=(const Stack& other); }; // 这里的 Stack 相当于 Stack<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 32 33 34 35 |
#include <iostream> #include <string> #include <type_traits> // 这里放前面的 Stack 定义和成员函数定义 int main() { Stack<int> numbers; numbers.push(10); numbers.push(20); numbers.push(30); std::cout << "top: " << numbers.top() << '\n'; std::cout << "size: " << numbers.size() << '\n'; numbers.pop(); std::cout << "after pop: "; numbers.print(std::cout); std::cout << '\n'; Stack<std::string> messages; std::string login = "login"; messages.push(login); // 复制 messages.push("logout"); // 构造临时 string 后移动 std::cout << "message: " << messages.top() << '\n'; static_assert( !std::is_same_v<Stack<int>, Stack<double>> ); } |
不同模板实参产生不同类型
|
1 2 3 4 |
// 它们是完全不同的类型 Stack<int> int_stack; Stack<long> long_stack; |
类模板的成员不会一次性全部实例化
- 假设
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
template<typename T> class Stack { public: void push(const T& value) { elements_.push_back(value); } void print(std::ostream& out) const { for (const auto& element : elements_) { out << element << '\n'; } } private: std::vector<T> elements_; }; |
- 定义一个不可输出的类型
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct Packet { int command; }; // 没有为 Packet 定义 operator<< // 但下面仍然可以编译 Stack<Packet> packets; packets.push(Packet{100}); // 因为使用 push() 不需要输出操作 // 只有调用 packets.print(std::cout); // 才会实例化 print() 的函数体,然后报错 |
- 但不是所有错误都会延迟
|
1 2 3 4 5 6 7 8 9 10 |
template<typename T> class Container { typename T::value_type value_; }; // 实例化 Container<int> c; // 会立即出错,因为形成完整的 Container<int> 时,数据成员声明必须合法,而: // int::value_type不存在 |
成员函数与成员函数模板不是一回事
- 这里的
push()依赖类模板参数T,但它本身没有自己的模板参数- 它是类模板的成员函数,不是独立的成员函数模板
|
1 2 3 4 5 6 |
template<typename T> class Stack { public: void push(const T& value); }; |
- 真正的成员函数模板还会再出现一层
template:T属于类模板U属于成员函数模板
|
1 2 3 4 5 6 7 8 |
template<typename T> template<typename U> void Stack<T>::push_converted(U&& value) { elements_.push_back( std::forward<U>(value) ); } |
push(T&&) 不是转发引用
- 这里的
T&&是普通右值引用,不是转发引用
|
1 2 3 4 5 6 |
template<typename T> class Stack { public: void push(T&& value); }; |
- 转发引用
|
1 2 |
template<typename U> void push(U&& value); // U 在调用时推导,是转发引用 |
- 判断方法:
- 如果
T在进入函数调用之前就已经由外层类固定了,那么T&&通常不是转发引用
- 如果
为什么模板实现通常放在头文件
- 只在头文件声明
|
1 2 3 4 5 6 7 |
// stack.hpp template<typename T> class Stack { public: void push(const T&); }; |
- 然后在
.cpp定义
|
1 2 3 4 5 |
// stack.cpp template<typename T> void Stack<T>::push(const T& value) { } |
- 但其他编译单元使用时通常看不到模板完整定义,无法实例化
Stack<int>::push(),最终容易出现链接错误
|
1 2 |
Stack<int> stack; stack.push(42); |
-
常见组织方式有两种
-
全部写入
.hpp -
声明和实现分成
.hpp、.tpp.tpp最终还是被头文件包含,所以使用点能看到完整定义
-
|
1 2 3 4 5 6 7 8 9 |
// stack.hpp template<typename T> class Stack { public: void push(const T&); }; #include "stack.tpp" |
|
1 2 3 4 5 |
// stack.tpp template<typename T> void Stack<T>::push(const T& value) { } |
类模板特化、默认参数、别名与 CTAD
主模板
- 如果不存在更匹配的特化版本,就使用主模板
|
1 2 3 4 5 6 |
template<typename T> class TypeInfo { public: static constexpr const char* name = "general type"; }; |
全特化
- 假设希望为
bool提供完全不同的实现
|
1 2 3 4 5 6 |
template<> class TypeInfo<bool> { public: static constexpr const char* name = "boolean type"; }; |
- 语法结构
- 这里写
template<>,因为已经没有需要继续推导的模板参数 - 所有模板实参都确定了,因此称为全特化或显式特化
- 这里写
|
1 2 3 4 |
template<> class 模板名<确定的模板实参> { }; |
全特化是一个独立的类
- 全特化不会继承主模板的成员
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
template<typename T> class Container { public: void push(const T&); void pop(); }; // Container<bool> 不会自动获得主模板中的 push() 和 pop() template<> class Container<bool> { public: void set(bool); }; |
|
1 2 3 4 5 6 |
Container<int> ints; ints.push(10); // 可以 Container<bool> flags; flags.set(true); // 可以 flags.push(true); // 错误:特化版本没有 push() |
全特化的成员函数定义
|
1 2 3 4 5 6 7 8 9 |
template<typename T> class Stack { public: void push(const T& value); private: std::vector<T> elements_; }; |
- 全特化
- 定义其成员函数时,不再写
template<> - 因为
Stack<std::string>已经是一个确定的类
- 定义其成员函数时,不再写
|
1 2 3 4 5 6 7 8 9 |
template<> class Stack<std::string> { public: void push(const std::string& value); private: std::deque<std::string> elements_; }; |
|
1 2 3 4 |
void Stack<std::string>::push(const std::string& value) { elements_.push_back(value); } |
- 头文件中的
ODR问题- 全特化的类定义可以放在头文件
- 类内定义的成员隐式为
inline
|
1 2 3 4 5 6 7 8 9 10 11 12 |
template<> class Stack<std::string> { public: void push(const std::string& value) { elements_.push_back(value); } private: std::deque<std::string> elements_; }; |
- 但如果在头文件中这样写:
- 已经是普通非模板函数定义
- 多个
.cpp包含后可能违反ODR
|
1 2 3 |
void Stack<std::string>::push(const std::string& value) { } |
|
1 2 3 4 5 6 |
inline void Stack<std::string>::push( const std::string& value) { } // 或者把定义放进一个 .cpp |
特化必须在首次实例化之前可见
- 全特化必须在第一次会导致隐式实例化的使用之前声明,并且各个编译单元必须看到一致的特化声明
|
1 2 3 4 5 6 7 8 9 10 11 12 |
template<typename T> class TypeInfo { }; // 已经使用,可能触发隐式实例化 TypeInfo<bool> info; template<> class TypeInfo<bool> // 太晚了 { }; |
- 通常把主模板和特化都放在相关头文件中
|
1 2 3 4 5 |
template<typename T> class TypeInfo; template<> class TypeInfo<bool>; |
偏特化
- 全特化处理一个确定类型
|
1 |
TypeInfo<bool> |
- 偏特化处理一整类具有某种结构的类型
|
1 2 3 4 5 6 7 8 |
// 例如,针对所有指针 template<typename T> class TypeInfo<T*> { public: static constexpr const char* name = "pointer type"; }; |
- 注意这里仍然存在未知类型
T- 所以前面必须保留
template<typename T>
- 所以前面必须保留
|
1 2 3 |
int* → T = int double* → T = double const char* → T = const char |
示例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
template<typename T> class TypeInfo { public: static constexpr const char* name = "general"; }; template<> class TypeInfo<bool> { public: static constexpr const char* name = "bool"; }; template<typename T> class TypeInfo<T*> { public: static constexpr const char* name = "pointer"; }; |
为什么类模板可以偏特化
- 简化版
is_pointer
|
1 2 3 4 5 6 7 8 9 10 11 |
#include <type_traits> template<typename T> struct IsPointer : std::false_type { }; template<typename T> struct IsPointer<T*> : std::true_type { }; |
|
1 2 |
IsPointer<const int*> 匹配 IsPointer<T*> 推导:T = const int |
- 顶层
const指针问题
|
1 2 3 4 |
using P = int* const; // 可能是 false,因为完整类型是“const 指针”,不能直接匹配 T* IsPointer<P>::value |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 可以先移除顶层 cv template<typename T> struct IsPointerImpl : std::false_type { }; template<typename T> struct IsPointerImpl<T*> : std::true_type { }; template<typename T> struct IsPointer : IsPointerImpl<std::remove_cv_t<T>> { }; |
多模板参数的偏特化
- 定义主模板
|
1 2 3 4 5 |
template<typename T1, typename T2> struct Relation { static constexpr const char* name = "general"; }; |
- 如果两个类型相同
|
1 2 3 4 5 |
template<typename T> struct Relation<T, T> { static constexpr const char* name = "same types"; }; |
- 如果第二个类型为
int
|
1 2 3 4 5 |
template<typename T> struct Relation<T, int> { static constexpr const char* name = "second is int"; }; |
偏特化可能产生歧义
- 两者没有谁明显比另一个更特殊,所以产生歧义
|
1 2 3 4 5 |
Relation<int, int> // 它同时匹配 Relation<T, T> // T = int Relation<T, int> // T = int |
- 可以为交集提供更具体的完全特化
|
1 2 3 4 5 |
template<> struct Relation<int, int> { static constexpr const char* name = "both int"; }; |
函数模板不能偏特化
- 类模板可以
|
1 2 3 4 5 |
template<typename T> struct Info; template<typename T> struct Info<T*>; // 偏特化 |
- 函数模板不能这样做
|
1 2 3 4 5 |
template<typename T> void process(T); template<typename T> void process<T*>(T*); // 错误:函数模板不能偏特化 |
- 函数模板通常使用重载
|
1 2 3 4 5 |
template<typename T> void process(T value); template<typename T> void process(T* value); |
默认类模板参数
- 可以把底层容器也参数化
|
1 2 3 4 5 6 7 8 9 10 11 |
#include <vector> template< typename T, typename Container = std::vector<T> > class Stack { private: Container elements_; }; |
|
1 2 3 4 5 |
// 使用默认容器 Stack<int> stack; // 等价于 Stack<int, std::vector<int>> stack; |
- 也可以覆盖默认参数
|
1 2 3 |
#include <deque> Stack<double, std::deque<double>> stack; |
- 如果两个都有默认值
|
1 2 3 4 5 6 7 8 |
template< typename T = int, typename Container = std::vector<T> > class Stack; // 才可以写 Stack<> stack; // Stack<int, std::vector<int>> |
类型别名
|
1 |
using IntStack = Stack<int>; |
别名模板
using可以被模板化
|
1 2 |
template<typename T> using DequeStack = Stack<T, std::deque<T>>; |
|
1 2 3 4 5 |
DequeStack<int> stack; // 等价于 Stack<int, std::deque<int>> stack; |
别名模板不能被特化
- 下面不允许
|
1 2 3 4 5 |
template<typename T> using Storage = std::vector<T>; template<> using Storage<bool> = std::deque<bool>; // 错误 |
- 如果需要特化,应增加一层类模板
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
template<typename T> struct StorageSelector { using type = std::vector<T>; }; template<> struct StorageSelector<bool> { using type = std::deque<bool>; }; template<typename T> using Storage = typename StorageSelector<T>::type; |
C++17类模板参数推导CTAD
C++17之前通常必须写
|
1 |
Stack<int> stack; |
C++17开始,如果构造函数参数能够推导模板参数,可以省略<...>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template< typename T, typename Container = std::vector<T> > class Stack { public: Stack() = default; explicit Stack(T value) { elements_.push_back(std::move(value)); } private: Container elements_; }; |
|
1 2 3 4 5 |
Stack stack{42}; // 编译器从构造函数 Stack(T value) 推导 T = int Container 使用默认值 std::vector<int> |
CTAD只是在构造对象时帮助确定类模板实参,并没有把Stack变成普通类型
CTAD与字符串字面量
- 假设构造函数按值接收
|
1 |
explicit Stack(T value); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 调用 Stack stack{"hello"}; // 函数模板按值推导会让数组退化,所以通常得到 T = const char* // 最终类型是 Stack<const char*> // 这通常不是我们希望的,因为它只保存裸指针,不拥有字符串内容 Stack(const char*) -> Stack<std::string>; // 调用 Stack stack{"hello"}; // 现在推导为 Stack<std::string> |
- 重要的一点
- 推导指引只决定构造哪个类型,不负责真正执行参数转换或对象构造
直接初始化与拷贝初始化
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 有推导指引 Stack(const char*) -> Stack<std::string>; // 下面通常可以 Stack stack{"hello"}; // 因为这是直接列表初始化,可以选择 // 并把 "hello" 转换为 std::string Stack<std::string>::Stack(std::string) // 但下面可能失败 Stack stack = "hello"; // 原因可能包括 // 构造函数是 explicit // 拷贝初始化不允许使用该显式构造函数 // 转换序列需要多个用户定义转换 |
示例
|
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
#include <deque> #include <iostream> #include <stdexcept> #include <string> #include <string_view> #include <utility> #include <vector> template<typename T> struct TypeInfo { static constexpr std::string_view name = "general"; }; template<> struct TypeInfo<bool> { static constexpr std::string_view name = "bool"; }; template<typename T> struct TypeInfo<T*> { static constexpr std::string_view name = "pointer"; }; template< typename T, typename Container = std::vector<T> > class Stack { public: Stack() = default; explicit Stack(T value) { push(std::move(value)); } void push(T value) { elements_.push_back(std::move(value)); } const T& top() const { if (elements_.empty()) { throw std::out_of_range("empty stack"); } return elements_.back(); } private: Container elements_; }; Stack(const char*) -> Stack<std::string>; template<typename T> using DequeStack = Stack<T, std::deque<T>>; int main() { std::cout << TypeInfo<int>::name << '\n'; std::cout << TypeInfo<bool>::name << '\n'; std::cout << TypeInfo<int*>::name << '\n'; Stack number{42}; // Stack<int> Stack text{"login"}; // Stack<std::string> DequeStack<double> values; values.push(3.14); std::cout << number.top() << '\n'; std::cout << text.top() << '\n'; std::cout << values.top() << '\n'; } |
其他
std::decay
std::decay从C++11引入,定义在#include <type_traits>
|
1 2 |
typename std::decay<T>::type // C++11 std::decay_t<T> // C++14 |
- 规则一:移除引用和顶层
cv- 普通类型会先移除引用,然后移除顶层的
const和volatile
- 普通类型会先移除引用,然后移除顶层的
|
1 2 3 4 5 |
std::decay_t<int> // int std::decay_t<const int> // int std::decay_t<volatile int> // int std::decay_t<const int&> // int std::decay_t<volatile int&&> // int |
|
1 2 3 4 5 |
const int& value = some_int; using T = std::decay_t<decltype(value)>; // decltype(value) = const int& // decay 后 = int |
|
1 2 3 4 5 6 7 8 |
// 只移除顶层 const // 这里的 const 修饰的是指针指向的 int,不是指针本身,所以不会移除 // 底层const留 using A = std::decay_t<const int*>; // const int* using B = std::decay_t<int* const>; // int* |
- 规则二:数组退化成元素指针
|
1 |
int buffer[1024]; |
|
1 |
decltype(buffer) // int[1024] |
|
1 |
std::decay_t<decltype(buffer)> // int* |
|
1 2 3 4 5 6 7 8 9 10 |
// 数组引用也会退化 using ArrayRef = int (&)[1024]; using Result = std::decay_t<ArrayRef>; int (&)[1024] ↓ 移除引用 int[1024] ↓ 数组退化 int* |
|
1 2 3 4 5 6 |
// 多维数组 using Array = int[2][3]; using Result = std::decay_t<Array>; Result = int (*)[3]; |
|
1 2 3 4 5 |
// 字符串字面量 decltype("hello") // const char (&)[6] std::decay_t<decltype("hello")> // const char* |
- 规则三:函数类型退化成函数指针
|
1 2 3 4 5 6 |
int process(double); decltype(process) // int(double) std::decay_t<decltype(process)> // int (*)(double) |
怎么理解decay
std::decay<T>大致模拟了:- 当一个对象按值传递或使用普通
auto保存时,类型会发生什么变化
- 当一个对象按值传递或使用普通
|
1 2 3 4 5 6 |
const int value = 10; const int& ref = value; auto copy = ref; // copy 的类型是 int |
|
1 2 3 |
// 对应 std::decay_t<decltype(ref)> // int |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ gflags记述:记录101/12
- ♥ Photoshop CEP扩展和插件开发04/27
- ♥ Json库RapidJson使用01/11
- ♥ C++17_第一篇12/20
- ♥ C++_成员访问权限06/20
- ♥ Soui应用 动画一06/24