惰性实例化与元函数组合
概述
- 示例
|
1 2 3 4 5 |
using Type = std::conditional_t< condition, 某个合法类型, 某个不合法但不会被选择的类型 >; |
- 很多人会认为“不选择的分支不会编译”,但实际经常仍然报错
- 关键规则是
std::conditional只负责选择已经成功形成的两个类型,不负责阻止模板实参本身被形成
什么是惰性实例化
- 考虑
|
1 2 3 4 5 6 |
template<class T> struct Explode { static_assert(sizeof(T) == 0); using type = int; }; |
- 仅仅把模板特化当成不完整类型使用,通常不需要实例化其定义
|
1 |
using Pointer = Explode<int>*; // 通常不触发 static_assert |
- 但访问成员必须实例化
|
1 2 |
using Type = typename Explode<int>::type; // 实例化 Explode<int>,static_assert 失败 |
- 其他需要完整类型的操作也会触发实例化,例如
|
1 2 3 4 |
sizeof(Wrapper<T>) Wrapper<T> object; 从 Wrapper<T> 继承 访问 Wrapper<T>::member |
conditional_t 为什么可能提前失败
- 错误示例
|
1 2 3 4 5 6 7 8 9 10 11 |
#include <type_traits> template<class T> using UnsignedOrSelfBad = std::conditional_t< std::is_integral_v<T> && !std::is_same_v<T, bool>, std::make_unsigned_t<T>, T >; |
|
1 2 3 4 5 |
// 我们希望 UnsignedOrSelfBad<int> // unsigned int UnsignedOrSelfBad<double> // double UnsignedOrSelfBad<bool> // bool |
- 但对于
double,即使条件是false,仍然必须先形成三个模板实参:
|
1 2 3 |
false std::make_unsigned_t<double> // 已经不合法 double |
- 编译器必须先知道传给
std::conditional_t的类型是什么,才能调用它进行选择
增加一层模板包装
- 不要提前计算
|
1 |
std::make_unsigned_t<T> |
- 而是把计算封装到类模板里
|
1 2 3 4 5 |
template<class T> struct MakeUnsignedDeferred { using type = std::make_unsigned_t<T>; }; |
- 对于保留原类型的分支
|
1 2 3 4 5 |
template<class T> struct Identity { using type = T; }; |
- 然后先选择包装器,再访问被选中包装器的
type
|
1 2 3 4 5 6 7 8 9 |
template<class T> using UnsignedOrSelf = typename std::conditional_t< std::is_integral_v<T> && !std::is_same_v<T, bool>, MakeUnsignedDeferred<T>, Identity<T> >::type; |
|
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 |
#include <type_traits> template<class T> struct MakeUnsignedDeferred { using type = std::make_unsigned_t<T>; }; template<class T> struct Identity { using type = T; }; template<class T> using UnsignedOrSelf = typename std::conditional_t< std::is_integral_v<T> && !std::is_same_v<T, bool>, MakeUnsignedDeferred<T>, Identity<T> >::type; int main() { static_assert( std::is_same_v< UnsignedOrSelf<int>, unsigned int > ); static_assert( std::is_same_v< UnsignedOrSelf<char>, unsigned char > ); static_assert( std::is_same_v< UnsignedOrSelf<double>, double > ); static_assert( std::is_same_v< UnsignedOrSelf<bool>, bool > ); } |
为什么增加包装后就能工作
|
1 2 3 |
// 对于 UnsignedOrSelf<double> // 条件为 false |
std::conditional_t接收的是- 这里只需要形成两个类模板特化的名字,通常不需要实例化它们的定义
|
1 2 |
MakeUnsignedDeferred<double> Identity<double> |
|
1 2 3 4 5 6 7 8 9 |
// 选择结果是 Identity<double> // 随后外层 typename ...::type // 只会要求被选中的 Identity<double>::type // 得到 double |
|
1 2 3 4 5 6 |
// 没有被选中的 MakeUnsignedDeferred<double> // 不需要访问 ::type,因此内部的 std::make_unsigned_t<double> // 不会被计算 |
- 关键结构是
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
typename select< 条件, 延迟计算A, 延迟计算B >::type::type // 第一层 type 选择元函数 // 第二层 type 执行选中的元函数 // 使用 conditional_t 后第一层已经由 _t 完成,所以代码表现为 typename std::conditional_t< 条件, 元函数A, 元函数B >::type |
C++20 std::type_identity
C++20标准库提供了
|
1 2 |
std::type_identity<T> std::type_identity_t<T> |
- 近似实现
|
1 2 3 4 5 6 7 8 9 |
template<class T> struct type_identity { using type = T; }; template<class T> using type_identity_t = typename type_identity<T>::type; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 因此可以把前面的代码改成: template<class T> using UnsignedOrSelf = typename std::conditional_t< std::is_integral_v<T> && !std::is_same_v<T, bool>, MakeUnsignedDeferred<T>, std::type_identity<T> >::type; // 这里必须使用包装类 std::type_identity<T> // 不能提前写成 std::type_identity_t<T> // 因为后者已经把包装拆开,直接产生了 T |
type_identity 还能阻止模板参数推导
- 考虑
|
1 2 3 4 5 |
template<class T> void add(T& destination, T value) { destination += value; } |
|
1 2 3 4 5 6 |
double result = 10.0; add(result, 1); // 推导发生冲突 第一个参数:T = double 第二个参数:T = int |
- 可以使用
type_identity_t让第二个位置不参与推导
|
1 2 3 4 5 6 7 8 9 |
#include <type_traits> template<class T> void add( T& destination, std::type_identity_t<T> value) { destination += value; } |
|
1 2 3 4 5 6 7 |
double result = 10.0; add(result, 1); // 现在过程是: 只通过第一个参数推导 T = double 第二个参数类型因此确定为 double 从 int 转换成 double |
- 这种位置叫非推导上下文
type_identity因而有两个常见作用- 包装类型,延迟计算
- 阻止某个位置参与模板参数推导
元函数转发
- 假设要组合两个转换
trait
|
1 2 3 4 5 6 7 8 |
template<class T> struct RemoveCVRef { using type = std::remove_cv_t< std::remove_reference_t<T> >; }; |
- 可以通过继承转发成员
|
1 2 3 4 5 6 7 |
template<class T> struct RemoveCVRef : std::remove_cv< std::remove_reference_t<T> > { }; |
|
1 2 |
// std::remove_cv<...> 已经提供 using type = ...; // 所以 RemoveCVRef 不必重复声明 |
|
1 2 3 4 5 6 |
static_assert( std::is_same_v< RemoveCVRef<const int&>::type, int > ); |
- 这叫元函数转发
- 新
trait继承另一个trait,让另一个trait的type、value等成员成为自己的结果
- 新
- 预测型
trait也一样
|
1 2 3 4 5 6 7 8 9 |
template<class T> struct IsRawPointer : std::is_pointer< std::remove_cv_t< std::remove_reference_t<T> > > { }; |
|
1 2 |
static_assert(IsRawPointer<int*&>::value); static_assert(!IsRawPointer<int&>::value); |
为什么别名模板不适合所有中间步骤
- 别名模板很好用
|
1 2 3 |
template<class T> using RemoveCVRefT = typename RemoveCVRef<T>::type; |
- 但它有两个重要限制:
- 一:别名模板不能特化
- 需要特化时,必须使用类模板,再在外面提供别名
|
1 2 3 4 5 6 7 |
// 不能写 template<class T> using Trait = ...; template<> using Trait<int> = ...; // 错误 |
- 二:别名通常会立即要求结果
|
1 2 3 |
template<class T> using MakeUnsignedNow = typename MakeUnsignedDeferred<T>::type; |
|
1 2 |
// 使用 MakeUnsignedNow<double> // 会立即访问 ::type,从而实例化内部计算 |
- 所以大型模板库通常同时提供
|
1 2 |
std::remove_reference<T> // 类模板,可延迟、可组合 std::remove_reference_t<T> // 最终取出结果 |
- 经验规则
- 中间组合阶段保留
trait包装器,最终需要实际类型时再使用_t
- 中间组合阶段保留
SFINAE-friendly trait
- 下面这个
trait看起来可以计算加法结果
|
1 2 3 4 5 6 7 8 9 |
template<class T1, class T2> struct BadPlusResult { using type = decltype( std::declval<T1>() + std::declval<T2>() ); }; |
|
1 2 3 4 5 6 7 8 9 10 |
// 对于 BadPlusResult<int, double> // 没有问题 // 但如果两个类型不支持 + struct A {}; struct B {}; BadPlusResult<A, B> // 实例化类模板定义时直接产生硬错误 |
- 更好的实现是
|
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 |
#include <type_traits> #include <utility> template<class T1, class T2, class = void> struct PlusResult { // 默认没有 type }; template<class T1, class T2> struct PlusResult< T1, T2, std::void_t< decltype( std::declval<T1>() + std::declval<T2>() ) > > { using type = decltype( std::declval<T1>() + std::declval<T2>() ); }; |
|
1 2 |
// 对于支持 + 的类型,偏特化匹配并提供 type // 对于不支持 + 的类型 PlusResult<A, B> 仍然是一个合法的类,只是没有 type 成员 |
- 这就是
SFINAE-friendly- 对不满足条件的合理输入,
trait本身尽量不要立即爆炸;它应产生false,或者安全地不提供结果成员
- 对不满足条件的合理输入,
检测SFINAE-friendly trait
- 可以实现一个通用的
HasType
|
1 2 3 4 5 6 7 8 9 10 11 12 |
template<class T, class = void> struct HasType : std::false_type { }; template<class T> struct HasType< T, std::void_t<typename T::type> > : std::true_type { }; |
|
1 2 3 4 5 6 7 8 9 10 |
struct A {}; struct B {}; static_assert( HasType<PlusResult<int, double>>::value ); static_assert( !HasType<PlusResult<A, B>>::value ); |
|
1 2 3 |
// 注意 PlusResult<A, B> 本身安全 // 但直接访问 typename PlusResult<A, B>::type // 在普通上下文中依然会报错,因为确实不存在 type |
SFINAE-friendly不是说任何错误都会消失,而是- 它允许外层模板在替换上下文中检测失败,并将候选排除
验证操作后才能继续计算属性
- 假设我们想判断移动构造是否为
noexcept
|
1 2 3 4 5 6 7 8 9 10 11 |
// 不安全版本 template<class T> struct IsNothrowMoveConstructible : std::bool_constant< noexcept( T(std::declval<T&&>()) ) > { }; |
- 如果类型根本不能移动构造,那么:
|
1 2 |
T(std::declval<T&&>()) // 本身就不合法,还没机会计算 noexcept |
- 安全版本分两步
- 先用
void_t检查构造表达式是否合法 - 只有合法时,偏特化才会被选择
- 在偏特化中计算该表达式是否为
noexcept - 不合法时回到主模板,结果为
false
- 先用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
template<class T, class = void> struct IsNothrowMoveConstructibleSafe : std::false_type { }; template<class T> struct IsNothrowMoveConstructibleSafe< T, std::void_t< decltype( T(std::declval<T&&>()) ) > > : std::bool_constant< noexcept( T(std::declval<T&&>()) ) > { }; |
- 标准库已经提供
|
1 2 |
std::is_move_constructible_v<T> std::is_nothrow_move_constructible_v<T> |
惰性布尔组合
C++17提供
|
1 2 3 |
std::conjunction<Traits...> std::disjunction<Traits...> std::negation<Trait> |
- 例如
|
1 2 3 4 5 6 |
template<class T> using IsSmallIntegral = std::conjunction< std::is_integral<T>, std::bool_constant<(sizeof(T) <= 4)> >; |
std::conjunction类似编译期逻辑与,并具有短路实例化语义- 找到第一个
value == false的 trait 后,后续 trait 不需要继续求值
- 找到第一个
std::disjunction找到第一个true后停止
if constexpr 是否能替代惰性trait
- 函数内部通常可以
- 这里未选中的语句被丢弃,所以比元函数包装直观
|
1 2 3 4 5 6 7 8 9 10 |
template<class T> void process(T value) { if constexpr (std::is_integral_v<T>) { using U = std::make_unsigned_t<T>; // ... } else { // 不会实例化 make_unsigned_t<T> } } |
- 但以下场景仍然需要
trait:- 计算函数返回类型
- 计算类模板成员类型
- 选择基类
- 选择成员存储类型
- 在函数声明中进行
SFINAE - 将类型计算结果传给其他模板
- 兼容
C++11/14
- 因此
- 函数实现分支:优先考虑
if constexpr - 类型层面的组合与接口声明:
trait和惰性实例化仍然重要 C++20接口约束:优先考虑Concepts
- 函数实现分支:优先考虑
线程池中的实际场景
- 线程池常见声明
|
1 2 3 4 5 |
template<class F, class... Args> auto submit(F&& function, Args&&... args) -> std::future< std::invoke_result_t<F&&, Args&&...> >; |
std::invoke_result是SFINAE-friendly trait- 如果调用表达式合法,它提供
type - 如果调用表达式不合法,它不提供
type
- 如果调用表达式合法,它提供
C++17可以结合
|
1 2 |
std::is_invocable_v<F&&, Args&&...> std::invoke_result_t<F&&, Args&&...> |
C++20更适合写- 这里先约束调用合法,再计算结果类型
|
1 2 3 4 5 6 |
template<class F, class... Args> requires std::invocable<F&&, Args&&...> auto submit(F&& function, Args&&... args) -> std::future< std::invoke_result_t<F&&, Args&&...> >; |
模板元编程基础——递归、编译期循环与 integer_sequence
概述
- 模板元编程(
TMP)指的是- 利用模板实例化,在编译期计算数值、产生类型或者生成重复代码
- 现代
C++中应当区分三种需求
| 需求 | 优先技术 |
| 计算编译期数值 | constexpr/consteval |
| 变换或筛选类型 | 类模板、偏特化、递归 |
| 展开参数包、元组、固定索引 | fold expression、index_sequence |
最经典的递归元函数:阶乘
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <cstddef> #include <iostream> #include <type_traits> template<std::size_t N> struct Factorial : std::integral_constant< std::size_t, N * Factorial<N - 1>::value > { }; template<> struct Factorial<0> : std::integral_constant<std::size_t, 1> { }; int main() { static_assert(Factorial<5>::value == 120); std::cout << Factorial<5>::value << '\n'; } |
|
1 2 3 4 5 6 7 8 9 |
// 展开过程 Factorial<5>::value = 5 * Factorial<4>::value = 5 * 4 * Factorial<3>::value = 5 * 4 * 3 * Factorial<2>::value = 5 * 4 * 3 * 2 * Factorial<1>::value = 5 * 4 * 3 * 2 * 1 * Factorial<0>::value = 120 |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 递归情况 template<std::size_t N> struct Factorial; // 不断请求 Factorial<N - 1> // 终止情况 // 通过全特化终止递归 template<> struct Factorial<0>; |
- 如果没有终止特化,编译器会不断实例化,最终超过模板实例化深度限制
数值计算优先使用 constexpr
- 传统模板递归是
C++98时代的主要编译期计算方式 - 现代
C++更适合写
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <cstdint> constexpr std::uint64_t factorial(unsigned n) { std::uint64_t result = 1; for (unsigned i = 2; i <= n; ++i) { result *= i; } return result; } static_assert(factorial(5) == 120); |
- 循环形式从
C++14开始可以方便地用于constexpr函数
|
1 2 3 4 5 6 |
// 它还可以在运行期使用 unsigned n; std::cin >> n; auto result = factorial(n); // 运行期计算 |
- 如果要求一定在编译期计算,
C++20可以使用
|
1 2 3 4 5 6 7 8 9 10 |
consteval std::uint64_t factorial(unsigned n) { std::uint64_t result = 1; for (unsigned i = 2; i <= n; ++i) { result *= i; } return result; } |
类型递归无法被普通函数代替
- 目标:无论数组有多少维,都删除全部数组维度
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <cstddef> #include <type_traits> template<class T> struct RemoveAllExtents { using type = T; }; template<class T, std::size_t N> struct RemoveAllExtents<T[N]> : RemoveAllExtents<T> { }; template<class T> struct RemoveAllExtents<T[]> : RemoveAllExtents<T> { }; template<class T> using RemoveAllExtentsT = typename RemoveAllExtents<T>::type; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
static_assert( std::is_same_v< RemoveAllExtentsT<int>, int > ); static_assert( std::is_same_v< RemoveAllExtentsT<int[3]>, int > ); static_assert( std::is_same_v< RemoveAllExtentsT<int[2][3][4]>, int > ); |
|
1 2 3 4 5 |
RemoveAllExtents<int[2][3][4]> → RemoveAllExtents<int[3][4]> → RemoveAllExtents<int[4]> → RemoveAllExtents<int> → int |
- 注意:
|
1 2 3 4 5 6 |
int (*)[3] // 是“指向数组的指针”,顶层类型是指针 // 因此不会匹配数组偏特化 RemoveAllExtentsT<int (*)[3]> // 仍然是 int (*)[3] |
- 标准库已经提供
|
1 |
std::remove_all_extents_t<T> |
编译期循环的本质
- 模板没有传统意义上的
|
1 2 |
for while |
- 传统模板元编程用递归表示循环
|
1 2 3 |
处理当前元素 → 对剩余元素实例化同一模板 → 用偏特化终止 |
- 而现代
C++更多使用参数包展开
|
1 2 3 |
一组编译期参数 → 使用 ... 展开 → 为每个参数生成一份表达式 |
|
1 2 3 4 5 |
template<class... Ts> void process(Ts... values) { (handle(values), ...); } |
|
1 2 3 4 5 |
// 这不是运行期循环。编译器会展开成 handle(value1); handle(value2); handle(value3); |
std::integer_sequence
C++14引入
|
1 |
std::integer_sequence<T, Values...> |
- 简化定义类似
|
1 2 3 4 5 6 7 8 9 10 |
template<class T, T... Values> struct integer_sequence { using value_type = T; static constexpr std::size_t size() noexcept { return sizeof...(Values); } }; |
|
1 2 3 4 5 6 7 8 9 10 11 |
using Sequence = std::integer_sequence<int, 2, 4, 6, 8>; // 2, 4, 6, 8 被编码在类型中 // Sequence 不是运行期数组 int values[] = {2, 4, 6, 8}; // 它更接近 一个携带非类型模板参数包的空类型 // 可以构造对象 Sequence{} |
index_sequence
- 索引最常使用
std::size_t,所以标准库提供简写
|
1 2 3 |
template<std::size_t... Indices> using index_sequence = integer_sequence<std::size_t, Indices...>; |
|
1 2 3 4 5 6 7 8 |
std::index_sequence<0, 1, 2, 3> // 等价于 std::integer_sequence< std::size_t, 0, 1, 2, 3 > |
- 标准库还可以自动生成
|
1 |
std::make_index_sequence<4> |
|
1 2 |
// 结果是 std::index_sequence<0, 1, 2, 3> |
|
1 2 3 4 |
// 注意上界不包含在序列中 make_index_sequence<N> 产生 0 到 N-1 |
|
1 2 3 4 5 |
// 特殊情况: make_index_sequence<0> // 得到空序列 std::index_sequence<> |
如何取出索引参数包
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> #include <utility> template<std::size_t... Indices> void print_indices( std::index_sequence<Indices...>) { ((std::cout << Indices << ' '), ...); std::cout << '\n'; } int main() { print_indices( std::make_index_sequence<5>{} ); } |
|
1 2 3 4 5 |
print_indices( std::index_sequence<0, 1, 2, 3, 4>{} ); 0 1 2 3 4 |
index_sequence_for
- 如果已经有一个类型参数包
|
1 |
class... Ts |
- 可以使用
|
1 |
std::index_sequence_for<Ts...> |
|
1 2 3 4 5 |
std::index_sequence_for<int, double, char> // 等价于 std::index_sequence<0, 1, 2> |
为什么元组需要索引序列
- 普通容器元素类型相同
|
1 2 3 |
vector[0] vector[1] vector[2] |
- 但
std::tuple中每个元素类型可能不同
|
1 2 3 4 5 |
std::tuple<int, std::string, bool> std::get<0>(tuple) std::get<1>(tuple) std::get<2>(tuple) |
- 这里的索引必须是编译期常量,不能写普通运行期循环
|
1 2 3 |
for (std::size_t i = 0; i < 3; ++i) { std::get<i>(tuple); // 错误,i 不是编译期常量 } |
|
1 2 3 4 5 |
// 因此需要先生成 std::index_sequence<0, 1, 2> // 再展开 std::get<Indices>(tuple)... |
实现 tuple_for_each
|
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 <cstdint> #include <functional> #include <iostream> #include <string_view> #include <tuple> #include <type_traits> #include <utility> template<class Tuple, class Function, std::size_t... Indices> constexpr void tuple_for_each_impl( Tuple&& tuple, Function&& function, std::index_sequence<Indices...>) { ( std::invoke( function, std::get<Indices>( std::forward<Tuple>(tuple) ) ), ... ); } template<class Tuple, class Function> constexpr void tuple_for_each( Tuple&& tuple, Function&& function) { using TupleType = std::decay_t<Tuple>; constexpr std::size_t size = std::tuple_size_v<TupleType>; tuple_for_each_impl( std::forward<Tuple>(tuple), std::forward<Function>(function), std::make_index_sequence<size>{} ); } int main() { auto packet_fields = std::tuple{ std::uint16_t{1}, std::uint32_t{128}, std::string_view{"login"} }; tuple_for_each( packet_fields, [](const auto& field) { std::cout << field << '\n'; } ); } |
为什么实现函数分成两层
- 公共函数只知道元组类型和长度
|
1 |
tuple_for_each(tuple, function); |
- 实现函数负责将一个序列类型拆成参数包
|
1 2 3 4 5 |
tuple_for_each_impl( tuple, function, std::index_sequence<Indices...> ); |
- 这种“两层函数”是大型模板代码中的固定模式
|
1 2 3 4 5 6 7 8 |
公共函数 → 计算 N → 生成 index_sequence<0...N-1> → 调用 impl impl 函数 → 捕获 Indices... → 执行包展开 |
值类别和生命周期
- 实现中使用
|
1 2 3 |
std::get<Indices>( std::forward<Tuple>(tuple) ) |
- 如果传入左值元组
- 元素作为左值传给函数
|
1 |
tuple_for_each(tuple, function); |
- 如果传入右值元组
- 元素可能以右值形式传给函数,可以被移动
|
1 2 3 4 |
tuple_for_each( std::make_tuple(...), function ); |
- 因此
tuple_for_each的回调如果保存元素引用,可能产生悬空引用
|
1 2 3 4 5 6 7 8 |
const std::string* saved = nullptr; tuple_for_each( std::make_tuple(std::string{"hello"}), [&](const auto& value) { // 如果保存 &value,调用结束后可能悬空 } ); |
std::apply 与索引序列
- 如果目的不是“逐个调用”,而是把元组元素一次性作为函数参数,可以直接使用
C++17
|
1 2 3 4 5 6 7 8 |
auto tuple = std::make_tuple(10, 20); auto result = std::apply( [](int first, int second) { return first + second; }, tuple ); |
- 因此:
- 一个函数接收所有元素:
std::apply - 同一个函数逐个处理元素:自己实现
tuple_for_each - 已经拥有参数包:直接
fold expression - 只有一个长度
N:先用make_index_sequence<N>
- 一个函数接收所有元素:
混合元编程:生成运行期代码
- 模板元编程不仅计算编译期数值,也可以根据编译期索引生成运行期运算
- 例如固定长度数组的点积
|
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 |
#include <array> #include <cstddef> #include <utility> template<class T, std::size_t N, std::size_t... Indices> constexpr T dot_product_impl( const std::array<T, N>& first, const std::array<T, N>& second, std::index_sequence<Indices...>) { return ( T{} + ... + (first[Indices] * second[Indices]) ); } template<class T, std::size_t N> constexpr T dot_product( const std::array<T, N>& first, const std::array<T, N>& second) { return dot_product_impl( first, second, std::make_index_sequence<N>{} ); } |
- 对于
N == 3,近似生成
|
1 2 3 4 |
return T{} + first[0] * second[0] + first[1] * second[1] + first[2] * second[2]; |
- 这里
- 索引和代码结构在编译期生成
- 数组元素可能是运行期数值
- 如果输入数组本身是
constexpr,结果也可以在编译期计算
不要为了“展开循环”盲目使用TMP
- 现代编译器通常可以
- 自动内联
- 自动展开循环
- 自动向量化
- 使用
SIMD指令
- 显式展开可能反而导致:
- 模板实例化数量增加
- 编译时间变长
- 二进制体积膨胀
- 指令缓存压力增加
- 错误信息变长
编译期循环方案对比
| 场景 | 推荐方式 |
| 编译期纯数值计算 | constexpr 循环 |
| 已有参数包 | fold expression |
| 元组逐元素处理 | index_sequence + 包展开 |
| 一个函数接收整个元组 | std::apply |
| 递归类型变换 | 类模板递归和偏特化 |
C++11/14 参数包副作用展开 |
数组/初始化列表技巧 |
| 运行期长度 | 普通 for/Ranges |
其他
阅读模板的翻译方法
- 现在执行这个类型函数
|
1 |
typename MetaFunction<T>::type |
- 作为另一个模板的参数时:
- 先传递尚未求值的类型函数包装器
|
1 |
MetaFunction<T> |
- 检查:
A<T>和B<T>只是包装器,还是已经通过_t/::type提前求值?
|
1 |
std::conditional_t<C, A<T>, B<T>> |
- 看到主模板没有
type- 默认情况下没有结果,只有满足检测条件的偏特化才提供结果
|
1 2 |
template<class T, class = void> struct Trait {}; |
- 看到继承
Trait正在转发OtherTrait<T>的type或value
|
1 |
struct Trait : OtherTrait<T> {}; |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ STL_deque05/18
- ♥ COM组件_101/31
- ♥ 文件md5值计算05/31
- ♥ C++_关于Async、Packaged_task、Promise的总结11/13
- ♥ CLion:配置C++下lua开发环境06/03
- ♥ C++_ 模板学习一08/06