非类型模板参数 NTTP
概述
- 前面使用的模板参数都是“类型”
|
1 2 |
template<typename T> class Stack; |
- 非类型模板参数传递的是编译期常量值
|
1 2 3 4 5 6 |
template<typename T, std::size_t Capacity> class FixedStack; // T 类型模板参数 int // Capacity 非类型模板参数 20 FixedStack<int, 20> 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 58 59 60 61 62 63 64 65 66 67 68 |
#include <array> #include <cstddef> #include <stdexcept> #include <utility> template<typename T, std::size_t Capacity> class FixedStack { static_assert(Capacity > 0, "FixedStack capacity must be greater than zero"); public: void push(T value) { if (size_ == Capacity) { throw std::length_error("FixedStack is full"); } elements_[size_] = std::move(value); ++size_; } void pop() { if (size_ == 0) { throw std::out_of_range("FixedStack is empty"); } --size_; } T& top() { if (size_ == 0) { throw std::out_of_range("FixedStack is empty"); } return elements_[size_ - 1]; } const T& top() const { if (size_ == 0) { throw std::out_of_range("FixedStack is empty"); } return elements_[size_ - 1]; } [[nodiscard]] bool empty() const noexcept { return size_ == 0; } [[nodiscard]] std::size_t size() const noexcept { return size_; } [[nodiscard]] static constexpr std::size_t capacity() noexcept { return Capacity; } private: std::array<T, Capacity> elements_{}; std::size_t size_{0}; }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> #include <string> int main() { FixedStack<int, 20> numbers; FixedStack<std::string, 40> messages; numbers.push(42); messages.push("login"); std::cout << numbers.top() << '\n'; std::cout << messages.top() << '\n'; static_assert(numbers.capacity() == 20); static_assert(messages.capacity() == 40); } |
值也是类型身份的一部分
- 下面是两个完全不同的类型
|
1 2 |
FixedStack<int, 20> FixedStack<int, 40> |
非类型模板实参必须是编译期常量
- 合法
|
1 2 3 |
constexpr std::size_t capacity = 1024; FixedStack<char, capacity> buffer; |
- 不合法
|
1 2 3 4 |
std::size_t capacity; std::cin >> capacity; FixedStack<char, capacity> buffer; // 错误 |
Capacity 不会作为普通成员存储
Capacity是编译期值,不需要额外增加std::size_t capacity_;
|
1 2 3 4 5 |
template<typename T, std::size_t Capacity> class FixedStack { std::array<T, Capacity> elements_; }; |
函数模板的非类型参数
|
1 2 3 4 5 |
template<int Increment, typename T> constexpr auto add_value(T value) { return value + Increment; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
auto a = add_value<5>(10); // 15 auto b = add_value<5>(10.5); // 15.5 auto c = add_value<-2>(10); // 8 // 推导过程 add_value<5>(10.5); Increment = 5 // 显式指定的编译期值 T = double // 根据函数实参推导 double add_value_5(double value) { return value + 5; } |
非类型模板参数也能从函数实参推导
- 最经典的是推导数组长度
|
1 2 3 4 5 |
template<typename T, std::size_t N> constexpr std::size_t array_size(const T (&)[N]) noexcept { return N; } |
|
1 2 3 4 |
int values[128]; static_assert(array_size(values) == 128); static_assert(array_size("hello") == 6); |
使用 auto 声明 NTTP
- 从
C++17开始,非类型模板参数的类型也可以用auto推导
|
1 2 3 4 5 6 7 |
template<auto Value> struct Constant { using value_type = decltype(Value); static constexpr auto value = Value; }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Constant<42> integer; Constant<42u> unsigned_integer; Constant<'A'> character; Constant<true> boolean; static_assert( std::is_same_v< Constant<42>::value_type, int > ); static_assert( std::is_same_v< Constant<42u>::value_type, unsigned int > ); |
固定为某个类型,还是使用 auto
- 指定明确类型
|
1 2 |
template<std::size_t Capacity> class Buffer; |
- 使用
auto
|
1 2 |
template<auto Value> struct Constant; |
允许哪些非类型模板参数
C++17及以前的主要类型- 整数类型
- 枚举
- 指针
- 左值引用
- 成员指针
std::nullptr_t
|
1 2 3 4 5 6 7 8 9 10 11 12 |
enum class Operation { read, write }; template<Operation Op> struct OperationPolicy { }; OperationPolicy<Operation::read> read_policy; |
C++20进一步支持- 浮点数
- 满足要求的结构化字面量类类型
- 无捕获
Lambda等结构化类型
|
1 2 3 4 5 6 7 |
template<double Scale> double scale_value(double value) { return value * Scale; } auto result = scale_value<1.5>(10.0); |
C++20使用字符串作为模板值
- 不能直接写
|
1 2 |
template<std::string Name> struct Message; // std::string 不是结构化类型,仍然不允许 |
- 但可以定义结构化的编译期字符串
- 它的成员:
- 是公开的
- 不是
mutable - 类型本身可以用于编译期比较和身份确定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <cstddef> template<std::size_t N> struct FixedString { char value[N]; constexpr FixedString(const char (&text)[N]) { for (std::size_t i = 0; i < N; ++i) { value[i] = text[i]; } } }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 于是可以写 template<FixedString Name> struct MessageTag { static constexpr auto name = Name; }; // 使用 using LoginTag = MessageTag<"Login">; using LogoutTag = MessageTag<"Logout">; static_assert(LoginTag::name.value[0] == 'L'); // 是两个不同类型 MessageTag<"Login"> MessageTag<"Logout"> |
- 这种技术在现代项目中用于
- 编译期字段名
- 日志标签
- 命令名称
- 反射模拟
- 编译期路由
SQL字段映射
指针和函数地址作为 NTTP
- 函数地址可以作为模板参数
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void on_login() { } void on_logout() { } template<auto Handler> struct Callback { static void invoke() { Handler(); } }; |
|
1 2 3 4 5 6 |
Callback<&on_login>::invoke(); Callback<&on_logout>::invoke(); // 或 Callback<on_login>::invoke(); |
- 这是一种编译期回调绑定
- 适用于:
- 协议命令静态分发
- 中断处理器绑定
- 固定回调策略
- 无运行期修改需求的状态机
|
1 2 3 4 |
// 是不同类型,不需要保存运行期函数指针成员 Callback<on_login> Callback<on_logout> |
引用作为 NTTP
- 也可以使用具有合适存储期的对象引用
|
1 2 3 4 5 6 7 8 9 10 11 12 |
int global_counter = 0; template<int& Counter> struct CounterUser { static void increment() { ++Counter; } }; CounterUser<global_counter> user; |
std::integral_constant
- 标准库把“类型”和“编译期值”绑定在一起
|
1 2 |
template<typename T, T Value> struct integral_constant; |
- 例如
|
1 2 3 4 |
using Answer = std::integral_constant<int, 42>; Answer::value // 42 Answer::value_type // int |
什么时候使用 NTTP
- 适合:
- 容量影响对象布局
- 数组长度需要保留在类型中
- 值需要参与编译期计算
- 只存在少量固定配置
- 希望不同配置成为不同类型
- 编译期策略或静态回调绑定
|
1 2 3 4 |
std::array<T, N> std::bitset<N> std::span<T, Extent> std::integral_constant<T, Value> |
- 不合适
- 值来自运行期
- 需要在运行期动态修改
- 可能出现大量不同值
- 不希望不同值产生不同类型
- 需要稳定、统一的
ABI接口
变参模板与参数包
概述
- 变参模板从
C++11引入,允许模板接收数量不固定、类型不同的参数
|
1 2 3 |
print(42); print(42, 3.14); print(42, 3.14, "login", std::string{"client"}); |
- 核心是
- 参数包代表编译期的一组参数,但不能把它当成普通变量使用;必须通过
...展开
- 参数包代表编译期的一组参数,但不能把它当成普通变量使用;必须通过
模板参数包与函数参数包
|
1 2 |
template<typename... Types> void function(Types... args); |
typename... Types- 是模板参数包,保存一组类型
Types... args- 是函数参数包,保存一组函数实参
|
1 2 3 4 5 |
function(42, 3.14, "hello"); // 按值推导后,概念上得到 Types = <int, double, const char*> args = <42, 3.14, "hello"> |
- 参数包只能在允许的语法位置展开或查询数量
递归展开参数包
C++11的经典实现
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> void print() { // 递归终止函数 } template<typename First, typename... Rest> void print(const First& first, const Rest&... rest) { std::cout << first << '\n'; print(rest...); } |
- 调用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
print(42, 3.14, "hello"); // 第一次实例化类似 print<int, double, char[6]>( 42, 3.14, "hello" ); // 执行 std::cout << 42 << '\n'; print(3.14, "hello"); // 第二次 std::cout << 3.14 << '\n'; print("hello"); // 第三次 // 最后调用无参数版本,递归结束 std::cout << "hello" << '\n'; print(); |
- 为什么要先取出第一个参数
|
1 2 3 4 5 6 7 |
template<typename First, typename... Rest> void print(const First& first, const Rest&... rest); first = 参数包中的第一个参数 rest = 剩下的所有参数 // 每次递归让参数包减少一个,直到为空 |
包展开是什么
...不只是展开一个名字,它会展开包含参数包的整个模式
|
1 |
print(rest...); |
- 示例
|
1 2 3 4 5 6 7 8 |
process(std::move(args)...); // 概念上展开为 process( std::move(arg1), std::move(arg2), std::move(arg3) ); |
|
1 2 3 4 5 6 7 8 9 |
print(args + args...); // print_doubled(1, 2.5, std::string{"ab"}); // 可能展开成 print( 1 + 1, 2.5 + 2.5, string{"ab"} + string{"ab"} ); |
sizeof...:获取参数个数
sizeof...(Types)- 模板类型参数个数
sizeof...(args)- 函数参数个数
|
1 2 3 4 5 6 |
template<typename... Types> void inspect(Types... args) { std::cout << sizeof...(Types) << '\n'; std::cout << sizeof...(args) << '\n'; } |
- 它得到的是参数数量,不是内存字节数
为什么普通 if 不能停止实例化
- 可能想这样写
- 当
Rest为空时,运行期条件恒为false,但普通if的函数体仍然必须通过编译 - 如果没有无参数重载,就会报错
- 当
|
1 2 3 4 5 6 7 8 9 |
template<typename First, typename... Rest> void print(const First& first, const Rest&... rest) { std::cout << first << '\n'; if (sizeof...(Rest) > 0) { print(rest...); } } |
C++17:使用if constexprif constexpr的未选择分支不会针对当前模板实例进行正常实例化,因此不需要无参数终止函数
|
1 2 3 4 5 6 7 8 9 |
template<typename First, typename... Rest> void print(const First& first, const Rest&... rest) { std::cout << first << '\n'; if constexpr (sizeof...(Rest) > 0) { print(rest...); } } |
C++17折叠表达式
- 递归模板能工作,但会产生很多递归实例
C++17引入折叠表达式,可以把某个二元运算符应用到整个参数包
|
1 2 3 4 5 |
template<typename... Values> auto sum(Values... values) { return (... + values); } |
|
1 2 3 4 5 |
// 调用 auto result = sum(1, 2, 3, 4); // 展开为 (((1 + 2) + 3) + 4) |
折叠表达式的四种形式
- 假设参数包为
|
1 |
p1, p2, p3 |
- “左折叠”和“右折叠”描述括号结合方向
| 形式 | 名称 | 展开结果 |
(... op pack) |
一元左折叠 | ((p1 op p2) op p3) |
(pack op ...) |
一元右折叠 | (p1 op (p2 op p3)) |
(init op ... op pack) |
二元左折叠 | ((init op p1) op p2) op p3 |
(pack op ... op init) |
二元右折叠 | p1 op (p2 op (p3 op init)) |
- 对减法影响明显
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 左折叠 template<typename... Values> auto subtract_left(Values... values) { return (... - values); } // 调用 subtract_left(10, 3, 2); // 展开 (10 - 3) - 2 // 结果 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// 右折叠 template<typename... Values> auto subtract_right(Values... values) { return (values - ...); } // 展开 10 - (3 - 2) // 结果 9 |
- 所以对于
-、/、赋值等非结合运算,左右折叠不能随意互换
处理空参数包
- 下面是一元折叠
- 如果参数包为空,没有任何元素可相加,因此表达式不合法
|
1 |
(... + values) |
- 可以提供初始值
- 所以二元折叠可以自然处理空包
|
1 2 3 4 5 |
template<typename... Values> auto sum(Values... values) { return (0 + ... + values); } |
- 少数一元折叠对空包有标准定义
| 运算符 | 空包结果 |
&& |
true |
| ` | |
, |
void() |
|
1 2 3 4 5 6 7 |
template<typename... Conditions> bool all(Conditions... conditions) { return (... && conditions); } static_assert(all()); |
用折叠表达式打印
- 不带分隔符
|
1 2 3 4 5 6 |
template<typename... Args> void print_joined(const Args&... args) { // 是带初始值 std::cout 的二元左折叠 (std::cout << ... << args) << '\n'; } |
|
1 2 3 4 5 6 |
print_joined("cmd=", 1, ", len=", 128); (((std::cout << "cmd=") << 1) << ", len=") << 128; |
- 每个参数后输出空格
|
1 2 3 4 5 6 |
template<typename... Args> void print_line(const Args&... args) { ((std::cout << args << ' '), ...); std::cout << '\n'; } |
|
1 2 3 4 5 6 7 8 9 |
// 展开的模式是 std::cout << args << ' ' // 整个模式通过逗号运算符展开 // 逗号运算符保证从左到右执行,因此输出顺序明确 (std::cout << arg1 << ' '), (std::cout << arg2 << ' '), (std::cout << arg3 << ' '); |
折叠括号不一定决定求值顺序
- 例如
|
1 |
(... + expressions) |
- 折叠表达式决定的是括号结合方式
|
1 |
((e1 + e2) + e3) |
- 但不代表所有运算符的操作数都一定按从左到右求值
- 如果参数表达式带有副作用,不要依赖
+的求值顺序
- 如果参数表达式带有副作用,不要依赖
- 需要严格顺序执行时,常使用
- 也就是对逗号运算符折叠
|
1 |
((operation(args)), ...); |
&&、还具有短路语义- 一旦前面的结果为
false,后续函数不会执行
- 一旦前面的结果为
|
1 2 3 4 5 |
template<typename... Predicates> bool all_true(Predicates... predicates) { return (... && predicates()); } |
所有参数类型是否相同
|
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <type_traits> template<typename First, typename... Rest> constexpr bool all_same_type() { return ( std::is_same_v<First, Rest> && ... ); } // 对于空的 Rest // 折叠的是空 &&,结果为 true |
- 如果根据函数实参推导,需要决定是否忽略
cv和引用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 比较的是对象基础类型,而不是精确的引用类型 template<typename First, typename... Rest> constexpr bool all_same_value_type( First&&, Rest&&... ) { using Base = std::remove_cvref_t<First>; return ( std::is_same_v< Base, std::remove_cvref_t<Rest> > && ... ); } |
完美转发参数包
- 大型项目里最常见的变参模板形式是
|
1 2 |
template<typename... Args> void function(Args&&... args); |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 其中 Args&&... args // 是一组转发引用 // 转发时写 std::forward<Args>(args)... // 展开成 std::forward<A1>(arg1), std::forward<A2>(arg2), std::forward<A3>(arg3) |
实现简化版 invoke
|
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)... ); } |
|
1 2 3 |
void on_message(int socket, const std::string& message); call(on_message, 10, std::string{"login"}); |
- 参数包展开成类似
- 同时保持每个实参原本的左值或右值属性
|
1 2 3 4 5 |
std::invoke( on_message, 10, temporary_string ); |
std::make_shared、emplace_back、std::thread和很多ASIO接口都使用这种结构
|
1 2 3 4 5 |
template<typename T, typename... Args> std::shared_ptr<T> make_shared(Args&&... args); template<typename... Args> reference emplace_back(Args&&... args); |
线程池中保存参数包
- 立即调用时可以直接转发
- 但线程池任务会延迟执行。如果只保存引用,任务执行时调用者对象可能已经销毁
- 解析
std::decay_t<F>按值保存可调用对象std::make_tuple()默认按值保存参数std::apply()将元组重新展开为函数参数- 延迟执行时不依赖调用者局部变量的引用
|
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 |
// 一种按值保存的 C++17 写法 #include <functional> #include <tuple> #include <type_traits> #include <utility> template<typename F, typename... Args> auto make_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) ); }; } |
- 使用
|
1 2 3 4 5 6 7 8 9 |
auto task = make_task( [](int socket, std::string message) { // 处理消息 }, 10, std::string{"login"} ); task(); |
- 如果确实需要引用语义,应显式使用
- 并确保对象生命周期覆盖任务执行时间
|
1 2 |
std::ref(object) std::cref(object) |
变参类模板
- 标准库中
|
1 2 3 4 5 6 7 8 9 |
std::tuple<int, std::string, double> // 可以概念性理解为 template<typename... Elements> class tuple; // 模板参数包 Elements... // 表示元组中所有元素类型 |
std::variant也类似
|
1 2 3 4 5 6 7 8 9 |
std::variant< Login, Logout, Heartbeat > // 概念上 template<typename... Alternatives> class variant; |
非类型参数包
- 参数包不一定保存类型,也可以保存编译期值
|
1 2 3 4 |
template<std::size_t... Indices> struct IndexList { }; |
|
1 2 3 4 |
IndexList<0, 2, 4> // 标准库中对应 std::index_sequence<0, 2, 4> |
- 利用索引包访问元组
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <tuple> template<typename Tuple, std::size_t... Indices> void print_selected( const Tuple& tuple, std::index_sequence<Indices...>) { ((std::cout << std::get<Indices>(tuple) << ' '), ...); std::cout << '\n'; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
auto values = std::make_tuple( 42, "login", 3.14, true ); print_selected( values, std::index_sequence<0, 2, 3>{} ); // 展开 std::get<0>(values) std::get<2>(values) std::get<3>(values) |
变参继承:Overloaded模式
- 现代
C++项目中经常看到
|
1 2 3 4 5 6 7 8 |
template<typename... Functions> struct Overloaded : Functions... { using Functions::operator()...; }; template<typename... Functions> Overloaded(Functions...) -> Overloaded<Functions...>; |
- 假设
|
1 2 3 4 5 6 7 8 9 |
Functions = <F1, F2, F3> // 继承列表 struct Overloaded : F1, F2, F3 // using 展开为 using F1::operator(); using F2::operator(); using F3::operator(); |
- 经常配合
std::variant
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <variant> std::variant<int, std::string> value = "hello"; std::visit( Overloaded{ [](int number) { std::cout << "int: " << number; }, [](const std::string& text) { std::cout << "string: " << text; } }, value ); |
按值、const 引用与转发引用
| 声明 | 类型行为 | 适用场景 |
Args... args |
复制/移动,发生 decay |
函数内部需要自己的值 |
const Args&... args |
不复制,不保留可修改性 | 只读打印、观察 |
Args&&... args |
可保留值类别 | 转发、构造、调用包装 |
- 这会复制所有参数,并使数组退化为指针
|
1 2 |
template<typename... Args> void print(Args... args); |
|
1 2 3 4 |
// 现代只读打印通常更适合 template<typename... Args> void print(const Args&... args); |
- 如果要把参数继续传递给其他函数,则通常使用
|
1 2 |
template<typename... Args> void wrapper(Args&&... args); |
依赖名称与模板消歧
什么是依赖名称
- 名字的含义依赖模板参数时,称为依赖名称
|
1 2 3 4 5 6 |
template<typename T> void process() { std::size_t n = 0; // 非依赖名称 typename T::value_type value; // 依赖名称 } |
std::size_t- 无论
T是什么,都表示同一个类型,因此是非依赖名称
- 无论
T::value_type- 必须等到
T确定后才能知道: value_type是否存在- 它是类型还是变量
- 是否可以访问
- 具体代表什么类型
- 必须等到
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct A { using value_type = int; }; struct B { static constexpr int value_type = 42; }; // A::value_type是类型 // B::value_type是静态数据成员 // 所以模板定义阶段不能单凭T::value_type确定它是什么 |
模板的两阶段查找
- 第一阶段:模板定义阶段
- 编译器检查:
- 基本语法
- 不依赖模板参数的名称
- 非依赖表达式
- 能够立即确定的类型和函数
|
1 2 3 4 5 6 |
template<typename T> void function(T value) { nonexistent(); // 与 T 无关,定义阶段就应报错 value.process(); // 依赖 T,通常推迟检查 } |
- 第二阶段:模板实例化阶段
- 编译器用
MyType替换T,然后检查object.process();是否有效
- 编译器用
|
1 |
function<MyType>(object); |
- 但即使某个名字要推迟查找,编译器在第一阶段仍然必须正确解析语法
- 模板定义阶段就必须确定
- 这正是需要
typename的原因
|
1 2 3 4 5 |
T::value_type* pointer; // 这里的 * 到底是 // 类型* 变量; // 静态成员 * 变量; |
typename:告诉编译器“这是类型”
|
1 2 3 4 5 6 7 8 9 10 |
template<typename T> void function() { T::value_type* pointer; } // 编译器默认可能将它理解为乘法 // T 的静态成员 value_type // 乘以 // 变量 pointer |
- 为了告诉编译器
T::value_type是类型,需要写
|
1 2 3 4 5 6 7 8 |
template<typename T> void function() { typename T::value_type* pointer; } // 现在明确表示 // pointer是指向 T::value_type 的指针 |
- 当一个限定名称同时满足:
- 依赖模板参数
- 表示一个类型
- 当前上下文无法自动确定它是类型
|
1 2 3 4 |
T::Something // 通常需要加 typename T::Something |
容器中的典型用法
|
1 2 3 4 5 6 7 8 9 10 11 |
template<typename Container> void print_container(const Container& container) { typename Container::const_iterator position; for (position = container.begin(); position != container.end(); ++position) { std::cout << *position << ' '; } } |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 现代代码可以用 auto 简化 template<typename Container> void print_container(const Container& container) { for (auto position = container.begin(); position != container.end(); ++position) { std::cout << *position << ' '; } } |
- 在以下情况仍会频繁看到
typename- 类型别名
- 返回类型
- 模板默认参数
AllocatorTraits- 迭代器类型
- 元编程
|
1 2 |
template<typename Container> using ValueType = typename Container::value_type; |
typename 不会保证类型存在
|
1 2 3 4 5 |
template<typename T> void function() { typename T::value_type value; } |
typename只告诉编译器- 按类型解析
T::value_type - 它不会自动让该类型存在
- 按类型解析
|
1 2 3 4 |
function<int>(); // 会报错,因为: // int::value_type不存在 |
- 同样,如果
value_type实际是一个静态变量
|
1 2 3 4 5 6 7 |
struct Test { static constexpr int value_type = 42; }; // 也会失败,因为 typename 要求它必须是类型 function<Test>(); |
哪些地方不需要或不能写 typename
- 非依赖类型不需要
|
1 2 3 4 |
std::vector<int>::value_type value; // 编译器已经知道 std::vector<int> // 是一个确定类型,可以立即查找 value_type |
- 基类列表中不能写
- 基类列表的位置本身要求一个类型,所以不写
typename
- 基类列表的位置本身要求一个类型,所以不写
|
1 2 3 4 |
template<typename T> class Derived : public T::Base { }; |
|
1 2 3 4 5 6 |
// 错误 template<typename T> class Derived : public typename T::Base { }; |
decltype本身已经表示类型
|
1 2 3 4 5 6 7 8 |
template<typename T> void function(T value) { using Result = decltype(value.process()); } // 不需要 // typename decltype(...) |
依赖基类为什么需要 this->
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
template<typename T> class Base { public: void send() { std::cout << "Base::send\n"; } }; template<typename T> class Derived : public Base<T> { public: void process() { send(); // 可能找不到 Base<T>::send() } }; |
Base<T>是依赖基类,因为具体基类取决于T- 模板定义阶段,编译器通常不会在依赖基类中查找未限定名称
send- 原因是
Base<T>以后可能存在特化
- 原因是
|
1 2 3 4 5 |
template<> class Base<int> { // 可能根本没有 send() }; |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 所以应该写成 template<typename T> class Derived : public Base<T> { public: void process() { this->send(); } }; |
this的类型依赖当前类模板特化,因此:- 成为依赖名称,其查找被推迟到实例化阶段
|
1 |
this->send() |
不写 this-> 可能调用错误函数
- 模板定义阶段,全局
send()可见,而依赖基类中的成员不参与普通未限定查找,于是可能绑定到::send()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void send() { std::cout << "global send\n"; } template<typename T> class Base { public: void send() { std::cout << "Base::send\n"; } }; template<typename T> class Derived : public Base<T> { public: void process() { send(); } }; |
this->、基类限定和 using
- 访问依赖基类成员有三种常见办法
- 使用
this->- 简洁
- 保留正常成员查找
- 对虚函数保留虚派发
|
1 2 |
this->send(); this->socket_; |
- 使用完整基类名
- 这种写法明确指定基类实现
|
1 |
Base<T>::send(); |
|
1 2 3 4 5 6 7 |
// 如果 send() 是虚函数 Base<T>::send(); // Base<T>::send();是限定调用,会显式调用基类版本,通常绕过虚派发 // 而 this->send(); // this->send();仍可发生虚派发 |
- 使用
usingusing把基类成员引入派生类作用域- 当基类中存在一组重载时,这种方式尤其有用
|
1 2 3 4 5 6 7 8 9 10 11 |
template<typename T> class Derived : public Base<T> { using Base<T>::send; public: void process() { send(); } }; |
.template:告诉编译器“这是成员模板”
- 假设
|
1 2 3 4 5 |
template<typename T> void process(T& object) { object.convert<int>(); } |
|
1 2 |
// 如果 object 类型依赖 T,编译器在定义阶段不知道 convert是不是成员模板 // 它可能把<解析为小于运算符 |
|
1 2 3 4 5 6 7 |
// 需要写 template<typename T> void process(T& object) { object.template convert<int>(); } |
template是消歧关键字,意思是:convert是一个模板,后面的<int>是模板实参列表
三种 template 消歧形式
- 对象访问
|
1 |
object.template convert<int>(); |
- 指针访问
|
1 |
pointer->template convert<int>(); |
- 作用域访问
T:::依赖限定符template:说明Rebind是模板<int>:模板实参typename:说明最终的type是类型
|
1 2 3 4 5 |
T::template Rebind<int> // 通常和 typename 组合 typename T::template Rebind<int>::type |
Allocator 中的真实模式
|
1 2 3 4 |
template<typename Allocator, typename Node> using NodeAllocator = typename std::allocator_traits<Allocator> ::template rebind_alloc<Node>; |
- 含义是
Allocator是模板参数allocator_traits<Allocator>因此是依赖类型rebind_alloc是它的成员别名模板<Node>是成员模板实参- 整个结果是一个类型
如果没有显式模板实参,通常不需要 .template
|
1 2 3 4 5 6 7 8 |
template<typename T> void process(T& object) { // 这里没有<int> // 编译器不需要区分 < 是模板尖括号还是小于号 // 因此通常不需要:object.template convert(42); object.convert(42); } |
- 只有显式写出成员模板实参时,消歧问题最典型
|
1 |
object.template convert<int>(42); |
示例
|
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 |
#include <iostream> #include <type_traits> template<typename T> class Base { public: using value_type = T; template<typename U> struct Rebind { using type = Base<U>; }; template<typename U> U convert(const T& value) const { return static_cast<U>(value); } void show() const { std::cout << "value=" << value_ << '\n'; } protected: T value_{42}; }; template<typename T> class Derived : public Base<T> { public: using ValueType = typename Base<T>::value_type; using OtherBase = typename Base<T> ::template Rebind<double> ::type; void run() { this->show(); double result = this->template convert<double>( this->value_ ); OtherBase other; std::cout << "converted=" << result << '\n'; static_assert( std::is_same_v<ValueType, T> ); } }; int main() { Derived<int> object; object.run(); } |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++_解码Toml文件08/14
- ♥ C++_关于Invoke01/31
- ♥ Effective C++_第二篇07/01
- ♥ gflags记述:记录210/09
- ♥ C++并发编程 _ 内存模型原子操作08/07
- ♥ cpp-httplib:概述使用10/30