成员模板、变量模板与模板模板参数
成员模板
- 普通类和类模板都可以包含成员模板
|
1 2 3 4 5 6 7 8 9 |
class Converter { public: template<typename To, typename From> To convert(const From& value) { return static_cast<To>(value); } }; |
|
1 2 3 4 |
Converter converter; // convert<double>是成员函数模板的一个特化 double result = converter.convert<double>(42); |
类模板中的成员模板
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
template<typename T> class Box { public: explicit Box(T value) : value_(std::move(value)) { } template<typename U> U convert() const { return static_cast<U>(value_); } private: T value_; }; |
|
1 2 3 4 |
Box<int> box{42}; double d = box.convert<double>(); long l = box.convert<long>(); |
|
1 2 3 4 |
// 这里有两层模板参数 T = int 由类模板 Box<int> 决定 U = double 由成员模板 convert<double> 决定 |
- 同一个
Box<int>对象可以调用不同的成员模板特化
类外定义成员模板
|
1 2 3 4 5 6 7 8 9 10 |
template<typename T> class Box { public: template<typename U> U convert() const; private: T value_{}; }; |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 类外定义需要写两层 template template<typename T> template<typename U> U Box<T>::convert() const { return static_cast<U>(value_); } // template<typename T> 属于类模板 Box<T> // template<typename U> 属于成员函数模板 convert<U>() |
不同类模板特化之间的转换
- 默认情况下它们是完全不同的类型:
|
1 2 |
Box<int> Box<double> |
- 可以用成员模板实现转换构造
|
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 |
#include <concepts> #include <utility> template<typename T> class Box { template<typename> friend class Box; public: explicit Box(T value) : value_(std::move(value)) { } template<typename U> requires std::convertible_to<U, T> Box(const Box<U>& other) : value_(other.value_) { } const T& value() const noexcept { return value_; } private: T value_; }; |
|
1 2 3 4 5 6 7 8 9 |
Box<int> source{42}; Box<double> target{source}; // 推导过程: // 目标类型:Box<double>,所以 T = double // 源类型:Box<int>,所以 U = int // int 可以转换为 double // 最终 target 仍然是 Box<double> |
为什么需要友元模板
Box<double>和Box<int>是不同的类- 一个类模板特化默认不能访问另一个特化的私有成员,所以声明:
|
1 2 3 4 5 6 |
// Box 的所有特化互为友元 template<typename> friend class Box; // 因此 Box<double> 可以访问 Box<int>::value_ |
- 也可以避免友元,改用公共接口:
- 公共接口耦合更低
- 友元方式适合底层容器、智能指针等需要高效访问内部状态的实现
|
1 |
value_(other.value()) |
成员模板赋值运算符
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
template<typename T> class Box { template<typename> friend class Box; public: template<typename U> requires std::assignable_from<T&, const U&> Box& operator=(const Box<U>& other) { value_ = other.value_; return *this; } private: T value_{}; }; |
|
1 2 3 4 5 6 7 8 9 10 |
// 类外定义形式 template<typename T> template<typename U> requires std::assignable_from<T&, const U&> Box<T>& Box<T>::operator=(const Box<U>& other) { value_ = other.value_; return *this; } |
|
1 2 3 4 |
Box<int> ints; Box<double> doubles; doubles = ints; |
成员模板不会取代特殊成员函数
- 这个构造函数模板不是
Box<T>的拷贝构造函数
|
1 2 3 4 5 6 7 |
template<typename T> class Box { public: template<typename U> Box(const Box<U>& other); }; |
- 真正的拷贝构造函数是
|
1 2 3 4 5 6 7 |
Box(const Box&); // 所以对于: Box<int> a; Box<int> b{a}; // 编译器通常仍使用隐式生成的,而不是成员模板 Box<int>::Box(const Box<int>&) |
- 同样,赋值运算符模板不会取代普通复制赋值运算符
|
1 |
Box& operator=(const Box&); |
- 总结
| 操作 | 同类型对象 | 不同特化对象 |
Box<int> → Box<int> |
普通拷贝/移动成员 | 通常不使用成员模板 |
Box<int> → Box<double> |
类型不同 | 使用成员模板 |
容易混淆的细节
- 构造函数模板虽然不是复制构造函数,但仍属于“用户声明的构造函数”,因此会阻止编译器自动生成默认构造函数
|
1 2 3 4 5 6 7 8 |
class X { public: template<typename T> X(T&& value); }; X x; // 错误:没有默认构造函数 |
|
1 2 3 |
// 显示添加 X() = default; |
万能构造函数可能抢走复制构造
|
1 2 3 4 5 6 |
class Person { public: template<typename T> Person(T&& value); }; |
|
1 2 3 4 5 6 7 8 |
// 对于 Person p1{/*...*/}; Person p2{p1}; // 模板可以推导 T = Person& // 从而形成 Person(Person&); |
- 它对非
const左值可能比隐式拷贝构造函数匹配得更好,导致模板构造函数被意外选中
|
1 |
Person(const Person&); |
|
1 2 3 4 5 6 7 8 9 10 |
// 现代写法应限制它 template<typename T> requires ( !std::same_as< std::remove_cvref_t<T>, Person > ) Person(T&& value); |
成员函数模板不能是虚函数
|
1 2 3 4 5 6 7 8 |
// 不合法 class Interface { public: template<typename T> virtual void process(T value); // 错误 }; |
- 原因是成员模板可以实例化出任意多个函数,而虚函数表需要在编译类时确定有限的虚函数集合
- 可以选择:
- 固定参数的虚函数
- 类型擦除
std::variantCRTP静态多态- 非虚成员模板调用虚拟核心接口
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Interface { public: template<typename T> void process(const T& value) { // 序列化成统一格式 process_bytes(/*...*/); } private: virtual void process_bytes(/*统一类型*/) = 0; }; |
成员函数模板的特化
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class BoolString { public: explicit BoolString(std::string value) : value_(std::move(value)) { } template<typename T = std::string> T get() const { return T(value_); } private: std::string value_; }; |
- 针对
bool完全特化
|
1 2 3 4 5 6 7 |
template<> inline bool BoolString::get<bool>() const { return value_ == "true" || value_ == "1" || value_ == "on"; } |
泛型Lambda 本质上也使用成员模板
|
1 2 3 |
auto add = [](auto left, auto right) { return left + right; }; |
- 编译器概念上生成一个匿名类
|
1 2 3 4 5 6 7 8 9 |
class CompilerGenerated { public: template<typename T1, typename T2> auto operator()(T1 left, T2 right) const { return left + right; } }; |
|
1 2 3 4 |
// 所以 // 调用的是闭包类中不同的 operator() 成员模板特化 add(1, 2); add(1.5, 2.5); |
变量模板
- 变量模板从
C++14引入
|
1 2 3 |
template<typename T> constexpr T pi = T{3.1415926535897932384626L}; |
|
1 2 3 |
double d = pi<double>; float f = pi<float>; long double ld = pi<long double>; |
- 每组模板实参产生一个变量特化
|
1 2 3 |
pi<double> pi<float> pi<long double> |
- 默认模板实参
|
1 2 3 |
template<typename T = long double> constexpr T pi = T{3.1415926535897932384626L}; |
|
1 2 3 4 5 |
// 使用默认类型仍然要写尖括号 auto value = pi<>; // 不能直接写 auto value = pi; // 错误:pi 是变量模板名称 |
标准库的 _v
C++11的类型萃取写法
|
1 2 |
std::is_same<T1, T2>::value std::is_const<T>::value |
C++17使用变量模板简化
|
1 2 |
std::is_same_v<T1, T2> std::is_const_v<T> |
|
1 2 3 4 5 |
// 标准库概念上定义 template<typename T1, typename T2> inline constexpr bool is_same_v = is_same<T1, T2>::value; |
头文件中的变量模板
- 现代
C++17代码推荐inline允许变量模板特化的定义安全出现在多个翻译单元中
|
1 2 3 |
template<typename T> inline constexpr T pi_v = T{3.1415926535897932384626L}; |
- 对于可修改的变量模板
|
1 2 |
template<typename T> inline std::size_t object_count = 0; |
|
1 2 3 4 5 |
// 每个类型有独立全局状态 // 它们不是同一个变量 object_count<int> object_count<double> |
- 多线程修改时仍然需要同步
|
1 2 |
template<typename T> inline std::atomic<std::size_t> object_count{0}; |
模板模板参数
- 普通模板参数接收一个类型
|
1 2 |
template<typename Container> class Stack; |
|
1 2 3 |
// 使用时必须传入具体类型 Stack<std::vector<int>> |
- 模板模板参数接收的是“模板本身”
|
1 2 3 4 |
template< typename T, template<typename...> class Container> class Stack; |
|
1 2 3 4 |
Stack<int, std::vector> // 这里传入的不是std::vector<int> // 而是std::vector // 类模板内部再用 T 实例化它 |
Stack 示例
|
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 |
#include <concepts> #include <deque> #include <iostream> #include <utility> #include <vector> template< typename T, template<typename...> class Container = std::deque > class Stack { template< typename, template<typename...> class > friend class Stack; public: void push(T value) { elements_.push_back(std::move(value)); } const T& top() const { return elements_.back(); } bool empty() const noexcept { return elements_.empty(); } template< typename U, template<typename...> class OtherContainer > requires std::convertible_to<U, T> Stack& operator=( const Stack<U, OtherContainer>& other) { Container<T> converted; for (const U& element : other.elements_) { converted.push_back( static_cast<T>(element) ); } elements_ = std::move(converted); return *this; } private: Container<T> elements_; }; |
|
1 2 3 4 5 6 7 8 9 10 11 |
int main() { Stack<int> integers; integers.push(1); integers.push(2); Stack<double, std::vector> doubles; doubles = integers; std::cout << doubles.top() << '\n'; } |
成员模板类外定义的完整语法
- 如果把赋值函数移到类外
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template< typename T, template<typename...> class Container > template< typename U, template<typename...> class OtherContainer > requires std::convertible_to<U, T> Stack<T, Container>& Stack<T, Container>::operator=( const Stack<U, OtherContainer>& other) { // ... return *this; } |
模板模板参数的匹配
std::vector的简化声明类似
|
1 2 |
template<typename T, typename Allocator = std::allocator<T>> class vector; |
- 它有两个类型模板参数
|
1 2 3 4 5 6 7 8 9 |
// 如果写 template< typename T, template<typename> class Container > class Stack; // 在旧标准和严格匹配规则下,可能无法匹配 std::vector,因为参数数量不同 |
|
1 2 3 4 5 6 7 |
// 使用变参形式更灵活 template< typename T, template<typename...> class Container > class Stack; |
- 从
C++17开始,这里的class也可以写成typename
|
1 2 3 4 5 |
template< typename T, template<typename...> typename Container > class Stack; |
为什么不能直接传 std::array
- 它的第二个参数是非类型模板参数
std::size_t N
|
1 2 |
template<typename T, std::size_t N> struct std::array; |
- 而模板模板参数要求所有参数都是类型
|
1 |
template<typename...> class Container |
|
1 2 3 |
// 所以不能直接写 Stack<int, std::array> stack; // 不匹配 |
|
1 2 3 4 5 6 7 |
// 可以定义单参数别名模板 template<typename T> using Array32 = std::array<T, 32>; // 然后 Stack<int, Array32> stack; |
- 不过
std::array没有push_back(),因此我们的Stack实现仍然不能使用它- 模板签名匹配不代表操作接口匹配
什么时候使用模板模板参数
- 适合:
- 内部需要用自己的
T实例化另一个模板 - 容器模板注入
Allocator/Policy模板Trait生成器- 元编程中的模板组合
- 内部需要用自己的
|
1 2 3 4 5 6 7 8 |
template< typename T, template<typename> class StoragePolicy > class ObjectPool { StoragePolicy<T> storage_; }; |
|
1 2 3 4 5 6 7 |
// 如果调用者已经有完整类型,普通类型参数通常更简单 template<typename Container> class Stack { Container elements_; }; |
万能引用、引用折叠与完美转发
为什么普通引用不够
- 假设目标函数有三个重载
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> struct Message { }; void consume(Message&) { std::cout << "non-const lvalue\n"; } void consume(const Message&) { std::cout << "const lvalue\n"; } void consume(Message&&) { std::cout << "rvalue\n"; } |
- 直接调用
|
1 2 3 4 5 6 7 |
Message message; const Message const_message; consume(message); // Message& consume(const_message); // const Message& consume(Message{}); // Message&& consume(std::move(message)); // Message&& |
- 现在增加包装函数
|
1 2 3 4 5 |
template<typename T> void relay(T&& value) { consume(value); } |
|
1 2 3 4 5 6 7 |
// 看起来 value 的声明可能是 T&&,但调用结果并不正确 relay(Message{}); // 进入 relay() 后,表达式 // value是一个有名字的变量,因此它是左值,最终调用 consume(Message&) // 而不是 consume(Message&&) |
- 核心规则
- 任何有名字的变量表达式都是左值,即使变量声明类型是右值引用
std::move 也不能解决所有情况
- 如果改成
|
1 2 3 4 5 |
template<typename T> void relay(T&& value) { consume(std::move(value)); } |
|
1 2 3 4 5 6 7 8 |
// 临时对象可以正确转发 relay(Message{}); // consume(Message&&) // 但普通左值也会被无条件转换成右值 Message message; relay(message); // 也会调用 consume(Message&&) // 这意味着 relay() 可能意外移动调用者仍然需要的对象 |
- 所以
- 始终当左值传递:不对
- 始终
std::move():也不对 - 需要根据调用者原本传入的是左值还是右值决定
- 这就是
std::forward<T>()的作用
标准写法
|
1 2 3 4 5 6 7 |
#include <utility> template<typename T> void relay(T&& value) { consume(std::forward<T>(value)); } |
|
1 2 3 4 5 6 7 |
Message message; const Message const_message; relay(message); // consume(Message&) relay(const_message); // consume(const Message&) relay(Message{}); // consume(Message&&) relay(std::move(message)); // consume(Message&&) |
什么是万能引用
- 必须满足两个关键条件
- 形式基本上是
T&& T是本次函数调用中直接推导出来的模板参数- 所以,不是所有
T&&都是万能引用
- 形式基本上是
|
1 2 |
template<typename T> void function(T&& value); |
- 类模板参数已经提前确定
|
1 2 3 4 5 6 |
template<typename T> class Box { public: void set(T&& value); }; |
|
1 2 3 4 |
Box<std::string> box; // 此时 T 已经固定为 std::string,所以成员函数是 void set(std::string&& value); |
- 添加了
const- 这是
const右值引用,不是万能引用
- 这是
|
1 2 |
template<typename T> void function(const T&& value); |
- 不是直接的模板参数
- 这里的类型依赖
T,但不是直接的T&&,也不是转发引用
- 这里的类型依赖
|
1 2 |
template<typename T> void function(typename T::value_type&& value); |
auto&& 也可以是万能引用
|
1 2 3 4 |
Message message; auto&& a = message; // Message& auto&& b = Message{}; // Message&& |
- 泛型
Lambda
|
1 2 3 4 5 |
auto relay = [](auto&& value) { consume( std::forward<decltype(value)>(value) ); }; |
|
1 2 3 4 5 6 |
// 这里没有可以直接写的模板参数 T,所以使用 decltype(value) relay(message); // 左值 relay(Message{}); // 右值 |
- 范围
for中也经常看到- 它可以适应容器迭代器返回的引用或代理类型
|
1 2 |
for (auto&& element : container) { } |
万能引用的特殊推导规则
- 假设
|
1 2 |
template<typename T> void relay(T&& value); |
- 传入非
const左值
|
1 2 |
Message message; relay(message); |
|
1 2 3 4 5 6 7 |
// 特殊规则推导 T = Message& // 注意:这是少数能把模板参数本身推导为引用的情况 // 形参原本是 T&& // 代入后 Message& && // 引用折叠 Message& |
- 传入
const左值
|
1 2 3 4 5 6 7 8 9 |
const Message message; relay(message); T = const Message& // 形参 const Message& && // 折叠 const Message& |
- 传入右值
|
1 2 3 4 5 6 7 8 |
relay(Message{}); // 推导 T = Message // 形参 Message&& // 不需要折叠 |
- 传入
std::move
|
1 2 |
Message message; relay(std::move(message)); |
|
1 2 |
T = Message 形参 = Message&& |
- 总结
| 调用实参 | T |
形参T&&最终类型 |
Message 左值 |
Message& |
Message& |
const Message 左值 |
const Message& |
const Message& |
Message{} |
Message |
Message&& |
std::move(message) |
Message |
Message&& |
const 右值 |
const Message |
const Message&& |
引用折叠规则
C++不允许直接声明引用的引用- 但模板替换、类型别名、
decltype等过程可能间接形成引用的引用,因此需要引用折叠规则
| 组合 | 折叠结果 |
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
形参类型和表达式值类别是两回事
|
1 2 3 4 |
template<typename T> void relay(T&& value) { } |
- 如果传入右值
|
1 2 3 4 5 6 |
relay(Message{}); // 形参声明类型是 Message&& // 但函数体中的表达式 value 仍然是左值,因为它有名字 |
std::move 的本质
std::move()本身不移动任何资源- 无条件把表达式转换成右值类别,表示“这个对象可以被移动”
|
1 2 3 4 5 6 7 8 |
template<typename T> std::remove_reference_t<T>&& move(T&& value) { return static_cast< std::remove_reference_t<T>&& >(value); } |
- 真正是否发生移动,取决于后面调用的构造函数或赋值运算符
- 如果类型没有移动构造函数,可能仍会复制
std::forward 的本质
- 可以把
std::forward<T>()简化理解为
|
1 |
static_cast<T&&>(value) |
- 原参数是左值
|
1 2 3 4 5 6 |
T = Message& static_cast<T&&>(value) // static_cast<Message& &&>(value) // 折叠 static_cast<Message&>(value) |
- 原参数是右值
|
1 2 3 4 |
T = Message static_cast<T&&>(value) // static_cast<Message&&>(value) |
const对象通常移动不了
|
1 2 3 |
const std::string text = "hello"; std::string result = std::move(text); |
std::move(text)的类型是
|
1 |
const std::string&& |
- 而常见移动构造函数是
- 它不能接收
const右值,因为移动通常需要修改源对象
- 它不能接收
|
1 |
std::string(std::string&&); |
|
1 2 3 4 |
// 因此可能改为调用 std::string(const std::string&); // 发生复制 |
变参完美转发
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <functional> #include <utility> template<typename F, typename... Args> decltype(auto) call( F&& function, Args&&... args) { return std::invoke( std::forward<F>(function), std::forward<Args>(args)... ); } |
异常和返回类型也应转发
- 更完整的
C++20调用包装器requires:目标函数必须可调用decltype(auto):保留目标函数的精确返回类型,包括引用noexcept(...):目标调用不抛异常时,包装器也声明不抛异常std::forward:保留参数值类别
|
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 <functional> #include <type_traits> #include <utility> template<typename F, typename... Args> requires std::invocable<F&&, Args&&...> decltype(auto) invoke_forward( F&& function, Args&&... args) noexcept( std::is_nothrow_invocable_v< F&&, Args&&... > ) { return std::invoke( std::forward<F>(function), std::forward<Args>(args)... ); } |
完美转发不等于自动安全
- “完美”只表示尽可能保持调用表达式的类型属性,不表示:
- 一定不会复制
- 一定会移动
- 生命周期安全
- 线程安全
- 所有权正确
- 不会发生隐式转换
- 不会产生悬空引用
|
1 2 3 4 5 6 7 8 |
template<typename T> decltype(auto) identity(T&& value) { return std::forward<T>(value); } // 函数返回了临时字符串的右值引用,但临时对象会在完整表达式结束时销毁,之后 ref 悬空 auto&& ref = identity(std::string{"hello"}); |
不要对同一个参数重复转发
- 如果调用者传入右值,第一次转发可能已经把资源移走,第二次看到的是
moved-from对象
|
1 2 3 4 5 6 |
template<typename T> void process(T&& value) { consume(std::forward<T>(value)); log(std::forward<T>(value)); // 危险 } |
- 更合理的是先决定所有权
|
1 2 3 4 5 6 7 8 9 |
template<typename T> void process(T&& value) { log(value); // 先按左值观察 consume( std::forward<T>(value) ); // 最后一次使用时再转发 } |
花括号初始化列表不能直接推导
|
1 2 |
template<typename T> void function(T&& value); |
- 下面通常无法推导
|
1 2 |
// {1,2,3} 本身没有普通表达式类型,无法推导 T function({1, 2, 3}); // 错误 |
- 需要专门重载
|
1 2 |
template<typename T> void function(std::initializer_list<T> values); |
- 或者显式构造
|
1 |
function(std::vector<int>{1, 2, 3}); |
万能引用构造函数抢占拷贝构造
|
1 2 3 4 5 6 7 8 |
class Person { public: template<typename T> explicit Person(T&& name); Person(const Person&); }; |
|
1 2 3 4 5 6 7 8 9 10 11 |
Person p1{"David"}; Person p2{p1}; // 对模板构造函数 T = Person& 参数类型折叠为 Person& // 这是精确匹配 // 普通复制构造函数 Person(const Person&) // 需要增加 const,因此模板可能更匹配并被选中 |
C++20约束
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template<typename T> requires ( !std::same_as< std::remove_cvref_t<T>, Person > && std::constructible_from< std::string, T > ) explicit Person(T&& name) : name_(std::forward<T>(name)) { } |
异步任务中的生命周期问题
- 线程池接口通常写成
|
1 2 |
template<typename F, typename... Args> void enqueue(F&& function, Args&&... args); |
- 接口入口可以完美转发,但任务会延迟执行,不能简单保存
|
1 |
[&function, &args...] // 很容易悬空 |
- 一种一次性任务的按值保存方式
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <tuple> template<typename F, typename... Args> auto make_one_shot_task( F&& function, Args&&... args) { return [ fn = std::decay_t<F>( std::forward<F>(function) ), values = std::make_tuple( std::forward<Args>(args)... ) ]() mutable -> decltype(auto) { return std::apply( std::move(fn), std::move(values) ); }; } |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++_关于Shared_ptr管理内存05/30
- ♥ STL_queue06/07
- ♥ Soui三05/19
- ♥ C++11_第四篇12/08
- ♥ Effective C++_第二篇07/01
- ♥ Soui六06/01