Typelist——编译期类型容器
概述
- 普通容器保存运行期对象
|
1 2 |
std::vector<int> std::list<std::string> |
Typelist保存的是一组类型- 它没有运行期元素,也不负责构造对象
- 所有内容都编码在模板参数包中
|
1 |
TypeList<int, double, std::string> |
C++标准库没有名为std::typelist的类型,但:std::tuple<Ts...>保存一组不同类型的对象std::variant<Ts...>保存其中一种类型的对象std::integer_sequence<T, Values...>保存一组编译期数值- 大型项目经常定义自己的
TypeList Boost.MP11等库提供了成熟的类型列表算法
最小TypeList
|
1 2 3 4 |
template<class... Types> struct TypeList { }; |
- 使用
|
1 2 3 4 |
using Empty = TypeList<>; using NumericTypes = TypeList<int, long, float, double>; |
Typelist本身通常是空类- 这个对象不包含
int、long、float、double对象
- 这个对象不包含
|
1 2 3 4 5 |
NumericTypes list; // 类型信息存在于: TypeList<int, long, float, double> // 这个类型的模板实参中 |
Typelist是不可变的
- 向列表添加类型不会修改原列表,而是生成一个新类型
|
1 2 3 |
using L1 = TypeList<int, double>; using L2 = PushFrontT<L1, bool>; |
|
1 2 |
L1 == TypeList<int, double> L2 == TypeList<bool, int, double> |
实现Size
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <cstddef> #include <type_traits> template<class List> struct Size; template<class... Types> struct Size<TypeList<Types...>> : std::integral_constant< std::size_t, sizeof...(Types) > { }; |
|
1 2 3 4 |
using List = TypeList<int, double, char>; static_assert(Size<List>::value == 3); static_assert(Size<TypeList<>>::value == 0); |
- 也可以提供变量模板
|
1 2 3 |
template<class List> inline constexpr std::size_t SizeV = Size<List>::value; |
|
1 |
static_assert(SizeV<List> == 3); |
实现Front
Front返回第一个类型
|
1 2 3 4 5 6 7 8 9 10 11 |
template<class List> struct Front; template<class Head, class... Tail> struct Front<TypeList<Head, Tail...>> { using type = Head; }; template<class List> using FrontT = typename Front<List>::type; |
|
1 2 3 4 5 6 7 8 |
using List = TypeList<int, double, char>; static_assert( std::is_same_v< FrontT<List>, int > ); |
|
1 2 3 4 5 6 7 8 9 |
// 拆包过程 TypeList<int, double, char> Head = int Tail... = double, char // 因此 type = Head = int |
- 对于空列表
- 没有对应偏特化,因此不合法
- 这表示
Front的前置条件是列表非空
|
1 |
FrontT<TypeList<>> |
实现PopFront
PopFront删除第一个类型,并生成新列表
|
1 2 3 4 5 6 7 8 9 10 11 12 |
template<class List> struct PopFront; template<class Head, class... Tail> struct PopFront<TypeList<Head, Tail...>> { using type = TypeList<Tail...>; }; template<class List> using PopFrontT = typename PopFront<List>::type; |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
template<class List> struct PopFront; template<class Head, class... Tail> struct PopFront<TypeList<Head, Tail...>> { using type = TypeList<Tail...>; }; template<class List> using PopFrontT = typename PopFront<List>::type; |
- 这里不是删除运行期对象,而是重新构造一个模板特化
|
1 |
TypeList<Tail...> |
实现PushFront
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template<class List, class NewType> struct PushFront; template<class... Types, class NewType> struct PushFront< TypeList<Types...>, NewType > { using type = TypeList<NewType, Types...>; }; template<class List, class NewType> using PushFrontT = typename PushFront<List, NewType>::type; |
|
1 2 3 4 5 6 7 8 9 10 11 |
using List = TypeList<int, double>; using Result = PushFrontT<List, bool>; static_assert( std::is_same_v< Result, TypeList<bool, int, double> > ); |
实现PushBack
- 对于参数包形式的
TypeList,可以直接展开
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template<class List, class NewType> struct PushBack; template<class... Types, class NewType> struct PushBack< TypeList<Types...>, NewType > { using type = TypeList<Types..., NewType>; }; template<class List, class NewType> using PushBackT = typename PushBack<List, NewType>::type; |
|
1 2 3 4 5 6 7 8 |
using List = TypeList<int, double>; static_assert( std::is_same_v< PushBackT<List, bool>, TypeList<int, double, bool> > ); |
基础操作完整程序
|
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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
#include <cstddef> #include <type_traits> template<class... Types> struct TypeList { }; template<class List> struct Size; template<class... Types> struct Size<TypeList<Types...>> : std::integral_constant< std::size_t, sizeof...(Types) > { }; template<class List> struct Front; template<class Head, class... Tail> struct Front<TypeList<Head, Tail...>> { using type = Head; }; template<class List> using FrontT = typename Front<List>::type; template<class List> struct PopFront; template<class Head, class... Tail> struct PopFront<TypeList<Head, Tail...>> { using type = TypeList<Tail...>; }; template<class List> using PopFrontT = typename PopFront<List>::type; template<class List, class NewType> struct PushFront; template<class... Types, class NewType> struct PushFront<TypeList<Types...>, NewType> { using type = TypeList<NewType, Types...>; }; template<class List, class NewType> using PushFrontT = typename PushFront<List, NewType>::type; template<class List, class NewType> struct PushBack; template<class... Types, class NewType> struct PushBack<TypeList<Types...>, NewType> { using type = TypeList<Types..., NewType>; }; template<class List, class NewType> using PushBackT = typename PushBack<List, NewType>::type; int main() { using Types = TypeList<int, double, char>; static_assert(Size<Types>::value == 3); static_assert( std::is_same_v< FrontT<Types>, int > ); static_assert( std::is_same_v< PopFrontT<Types>, TypeList<double, char> > ); static_assert( std::is_same_v< PushFrontT<Types, bool>, TypeList<bool, int, double, char> > ); static_assert( std::is_same_v< PushBackT<Types, bool>, TypeList<int, double, char, bool> > ); } |
实现 At:按索引取类型
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
template<class List, std::size_t Index> struct At; template<class Head, class... Tail> struct At<TypeList<Head, Tail...>, 0> { using type = Head; }; template< std::size_t Index, class Head, class... Tail > struct At< TypeList<Head, Tail...>, Index > : At<TypeList<Tail...>, Index - 1> { }; template<class List, std::size_t Index> using AtT = typename At<List, Index>::type; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using Types = TypeList<int, double, char>; static_assert( std::is_same_v< AtT<Types, 0>, int > ); static_assert( std::is_same_v< AtT<Types, 1>, double > ); static_assert( std::is_same_v< AtT<Types, 2>, char > ); |
|
1 2 3 4 |
At<TypeList<int, double, char>, 2> → At<TypeList<double, char>, 1> → At<TypeList<char>, 0> → char |
- 如果索引越界,递归最终作用于空列表,编译失败
- 标准库对于元组有相似操作
|
1 |
std::tuple_element_t<Index, Tuple> |
Transform:转换每个类型
- 目标
|
1 |
TypeList<int, double, char> |
|
1 2 3 |
// 经过 std::add_pointer 转换成 TypeList<int*, double*, char*> |
- 实现
|
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 |
template< class List, template<class> class MetaFunction > struct Transform; template< template<class> class MetaFunction, class... Types > struct Transform< TypeList<Types...>, MetaFunction > { using type = TypeList< typename MetaFunction<Types>::type... >; }; template< class List, template<class> class MetaFunction > using TransformT = typename Transform< List, MetaFunction >::type; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using Types = TypeList<int, double, char>; using PointerTypes = TransformT< Types, std::add_pointer >; static_assert( std::is_same_v< PointerTypes, TypeList<int*, double*, char*> > ); |
- 核心展开
|
1 |
typename MetaFunction<Types>::type... |
|
1 2 3 |
typename MetaFunction<int>::type, typename MetaFunction<double>::type, typename MetaFunction<char>::type |
- 我们的
Transform要求元函数遵循trait接口
|
1 2 3 4 5 |
template<class T> struct MetaFunction { using type = ...; }; |
Transform可能提前失败
- 下面会失败
|
1 2 3 4 5 6 7 8 |
using Types = TypeList<int, double>; using UnsignedTypes = TransformT< Types, std::make_unsigned >; |
|
1 |
// 因为 std::make_unsigned<double> 不合法 |
Transform会对列表中的每个元素实例化元函数- 它不会自动跳过不适用的类型
- 正确流程通常是
|
1 2 |
先 Filter 筛出合法类型 再 Transform 执行转换 |
Filter:过滤类型
- 目标
|
1 |
TypeList<int, double, char, std::string> |
|
1 2 3 4 5 6 |
// 保留满足 std::is_integral<T>::value // 的类型 // 得到 TypeList<int, char> |
- 实现
|
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 |
template< class List, template<class> class Predicate > struct Filter; template<template<class> class Predicate> struct Filter<TypeList<>, Predicate> { using type = TypeList<>; }; template< template<class> class Predicate, class Head, class... Tail > struct Filter< TypeList<Head, Tail...>, Predicate > { private: using FilteredTail = typename Filter< TypeList<Tail...>, Predicate >::type; public: using type = std::conditional_t< Predicate<Head>::value, PushFrontT< FilteredTail, Head >, FilteredTail >; }; template< class List, template<class> class Predicate > using FilterT = typename Filter<List, Predicate>::type; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <string> using Types = TypeList< int, double, char, std::string >; using IntegralTypes = FilterT< Types, std::is_integral >; static_assert( std::is_same_v< IntegralTypes, TypeList<int, char> > ); |
Filter如何保持顺序
- 输入
|
1 |
TypeList<int, double, char> |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 递归先处理尾部 TypeList<double, char> // 得到 TypeList<char> // 然后处理头部 int std::is_integral_v<int> == true // 将 int 添加到已过滤尾部的前面 PushFront<TypeList<char>, int> // 得到 TypeList<int, char> |
- 所以原有相对顺序得到保留
自定义Predicate
Predicate是输入类型、输出布尔值的元函数
|
1 2 3 4 5 6 7 |
template<class T> struct IsSmall : std::bool_constant< (sizeof(T) <= 8) > { }; |
|
1 2 |
using SmallTypes = FilterT<Types, IsSmall>; |
- 注意
sizeof(T)不等于复制成本,也不能完整描述:- 构造是否昂贵
- 是否动态分配内存
- 是否可平凡复制
- 是否适合按值传递
组合Transform和Filter
- 假设输入
|
1 2 3 4 5 6 7 |
using RawTypes = TypeList< const int&, double&&, const char, std::string& >; |
- 先删除
cv/ref
|
1 2 3 4 5 |
using CleanTypes = TransformT< RawTypes, std::remove_cvref >; |
|
1 2 3 4 5 6 7 8 |
// C++20 结果 TypeList< int, double, char, std::string > |
- 再过滤整数
|
1 2 3 4 5 |
using IntegralTypes = FilterT< CleanTypes, std::is_integral >; |
|
1 2 3 |
// 结果 TypeList<int, char> |
|
1 2 3 |
// 如果先过滤 FilterT<RawTypes, std::is_integral> // const int& 不是整数类型本身,而是引用类型,因此不会满足 is_integral |
把TypeList转成tuple或variant
Typelist只描述类型- 最终通常需要把它应用到实际类模板上
- 实现
Rename
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
template< class List, template<class...> class Target > struct Rename; template< template<class...> class Target, class... Types > struct Rename< TypeList<Types...>, Target > { using type = Target<Types...>; }; template< class List, template<class...> class Target > using RenameT = typename Rename<List, Target>::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 |
#include <tuple> #include <variant> using Types = TypeList<int, double, char>; using Tuple = RenameT<Types, std::tuple>; using Variant = RenameT<Types, std::variant>; static_assert( std::is_same_v< Tuple, std::tuple<int, double, char> > ); static_assert( std::is_same_v< Variant, std::variant<int, double, char> > ); |
- 这种操作在不同库中可能叫
RenameRebindApplyInstantiateWrap
- 可以理解为
- 把类型列表中的参数包重新传给另一个类模板
TypeList、Tuple和Variant的区别
| 类型 | 是否保存对象 | 运行期含义 |
TypeList<Ts...> |
否 | 纯编译期类型集合 |
std::tuple<Ts...> |
是 | 同时保存每种类型的一个对象 |
std::variant<Ts...> |
是 | 任意时刻保存其中一种类型 |
std::integer_sequence<T, Vs...> |
否 | 编译期数值序列 |
- 例如
|
1 2 3 4 5 6 7 8 |
// 不会构造任何对象 TypeList<int, std::string> // 会同时包含一个 int 和一个 std::string std::tuple<int, std::string> // 只保存一个当前活动值 std::variant<int, std::string> |
- 另外,
std::variant的备选类型不能是:void- 引用类型
- 数组类型
- 如果
TypeList经过转换后要生成variant,必须保证结果满足这些要求
把类型列表展开为代码
- 可以将每个类型包装成
tag,再执行参数包展开
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template<class T> struct TypeTag { using type = T; }; template<class... Types, class Function> constexpr void for_each_type( TypeList<Types...>, Function&& function) { ( function(TypeTag<Types>{}), ... ); } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> using Types = TypeList<int, double, char>; for_each_type( Types{}, [](auto tag) { using T = typename decltype(tag)::type; std::cout << sizeof(T) << '\n'; } ); |
- 这里没有创建
T对象,只构造了
|
1 |
TypeTag<T> |
C++20也可以使用
|
1 |
std::type_identity<T>{} |
- 这种技术可以用于
- 为一组消息类型生成处理器
- 为一组测试类型生成测试用例
- 为所有协议类型生成序列化函数表
- 对一组组件类型执行静态注册
网络消息中的实际应用
|
1 2 3 4 5 6 7 8 9 10 11 12 |
struct Login {}; struct Logout {}; struct Heartbeat {}; struct DebugMessage {}; using AllMessages = TypeList< Login, Logout, Heartbeat, DebugMessage >; |
- 定义
trait
|
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 |
template<class Message> struct MessageTraits; template<> struct MessageTraits<Login> { static constexpr bool wire_enabled = true; }; template<> struct MessageTraits<Logout> { static constexpr bool wire_enabled = true; }; template<> struct MessageTraits<Heartbeat> { static constexpr bool wire_enabled = true; }; template<> struct MessageTraits<DebugMessage> { static constexpr bool wire_enabled = false; }; |
- 定义
Predicate
|
1 2 3 4 5 6 7 |
template<class Message> struct IsWireMessage : std::bool_constant< MessageTraits<Message>::wire_enabled > { }; |
- 过滤
|
1 2 3 4 5 |
using WireMessages = FilterT< AllMessages, IsWireMessage >; |
|
1 2 3 4 5 6 7 |
// 结果 TypeList< Login, Logout, Heartbeat > |
- 生成
variant
|
1 2 3 4 5 |
using MessageVariant = RenameT< WireMessages, std::variant >; |
|
1 2 3 4 5 6 |
using MessageVariant = std::variant< Login, Logout, Heartbeat >; |
|
1 2 3 4 5 6 7 8 |
// 可以使用 std::visit( [](const auto& message) { handle(message); }, message_variant ); |
编译成本与现代化建议
- 对于具体的参数包
TypeListSize:直接sizeof...- `
Front:一次偏特化 PopFront:一次偏特化PushFront、PushBack:一次参数包展开Transform:每个类型实例化一次元函数Filter:线性递归,约为O(N)层实例化At<N>:线性递归,约为O(N)
- 大型列表的递归算法可能导致
- 编译时间增加
- 模板实例化深度过大
- 错误信息冗长
- 编译器内存消耗增加
std::tuple与异质存储
概述
std::tuple<Ts...>在一个对象中,分别保存参数包Ts...中的每一种对象- 它与之前的类型列表有本质区别
|
1 2 |
TypeList<int, std::string, double> // 只有类型信息,不保存对象 std::tuple<int, std::string, double> // 实际保存三个对象 |
std::tuple的基本使用
message中三个元素的类型完全不同
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> #include <string> #include <tuple> int main() { std::tuple<int, std::string, double> message{ 42, "login", 3.5 }; std::cout << std::get<0>(message) << '\n'; std::cout << std::get<1>(message) << '\n'; std::cout << std::get<2>(message) << '\n'; } |
- 这里的
0是模板实参,必须在编译期确定
|
1 2 3 4 5 6 7 8 9 10 11 |
std::get<0>(message); // 下面这样不行 std::size_t index = 1; std::get<index>(message); // 错误 // 原因是返回类型依赖索引 get<0> -> int& get<1> -> std::string& get<2> -> double& // 如果索引运行时才知道,那么函数的返回类型也无法在编译期确定 |
从零实现一个递归TinyTuple
|
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 |
#include <utility> template<class... Types> class TinyTuple; // 递归终点 template<> class TinyTuple<> { }; // 至少包含一个元素 template<class Head, class... Tail> class TinyTuple<Head, Tail...> { private: Head head_; TinyTuple<Tail...> tail_; public: TinyTuple(Head head, Tail... tail) : head_(std::move(head)), tail_(std::move(tail)...) { } Head& head() noexcept { return head_; } const Head& head() const noexcept { return head_; } TinyTuple<Tail...>& tail() noexcept { return tail_; } const TinyTuple<Tail...>& tail() const noexcept { return tail_; } }; |
|
1 2 3 |
TinyTuple<int, std::string, double> t{ 42, "login", 3.5 }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
TinyTuple<int, std::string, double> // 近似展开 class Tuple0 { int head_; class Tuple1 { std::string head_; class Tuple2 { double head_; TinyTuple<> tail_; } tail_; } tail_; }; |
实现get<I>
- 可以利用
if constexpr递归查找第I个元素
|
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 <cstddef> template<std::size_t I, class Head, class... Tail> decltype(auto) tiny_get(TinyTuple<Head, Tail...>& tuple) { static_assert(I < 1 + sizeof...(Tail), "tuple index out of range"); if constexpr (I == 0) { return tuple.head(); } else { return tiny_get<I - 1>(tuple.tail()); } } template<std::size_t I, class Head, class... Tail> decltype(auto) tiny_get(const TinyTuple<Head, Tail...>& tuple) { static_assert(I < 1 + sizeof...(Tail), "tuple index out of range"); if constexpr (I == 0) { return tuple.head(); } else { return tiny_get<I - 1>(tuple.tail()); } } |
|
1 2 3 4 5 6 7 8 9 |
TinyTuple<int, std::string, double> t{ 42, "login", 3.5 }; tiny_get<0>(t) = 100; tiny_get<1>(t) += "_request"; std::cout << tiny_get<0>(t) << '\n'; std::cout << tiny_get<1>(t) << '\n'; |
|
1 2 3 4 5 6 7 |
// 对于 tiny_get<2>(t) // 编译器进行的逻辑递归是 tiny_get<2>(TinyTuple<int, string, double>) tiny_get<1>(TinyTuple<string, double>) tiny_get<0>(TinyTuple<double>) return double& |
- 因为
if constexpr不会实例化未选择的分支,所以递归能在I == 0时停止
为什么返回 decltype(auto)
- 假设错误地写成
|
1 2 |
template<std::size_t I, class... Types> auto tiny_get(TinyTuple<Types...>& tuple); |
- 即使内部返回的是引用
- 普通
auto也会像变量类型推导一样丢掉引用,结果可能成为值类型
- 普通
|
1 |
return tuple.head(); |
- 而
decltype(auto)- 使用
decltype规则推导,可以保留返回表达式的引用性质
- 使用
- 标准库
std::get大致要保留这些类型
tuple表达式 |
普通元素对应的返回类型 |
tuple& |
T& |
const tuple& |
const T& |
tuple&& |
T&& |
const tuple&& |
const T&& |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
std::tuple<std::string> t{"hello"}; static_assert(std::is_same_v< decltype(std::get<0>(t)), std::string& >); static_assert(std::is_same_v< decltype(std::get<0>(std::move(t))), std::string&& >); // 因此,真正的 get 实现还需要处理右值和完美转发 |
按类型访问:std::get<T>
- 除了索引,标准库还支持
|
1 2 3 4 5 |
std::tuple<int, std::string, double> t{ 42, "login", 3.5 }; std::string& name = std::get<std::string>(t); |
- 但前提是目标类型在
tuple中恰好出现一次
|
1 2 3 4 5 |
std::tuple<int, int> t{1, 2}; std::get<0>(t); // 正确 std::get<1>(t); // 正确 std::get<int>(t); // 错误:int 出现了两次 |
- 如果一个返回值具有稳定的业务含义,工程中经常应该考虑使用结构体:
tuple更适合局部组合、泛型工具和临时返回值,不适合代替所有业务结构体
|
1 2 3 4 5 6 7 8 |
struct DecodeResult { std::uint16_t command; std::uint32_t payload_size; }; // 它通常比下面更清楚 using DecodeResult = std::tuple<std::uint16_t, std::uint32_t>; |
简单递归实现的空间问题
|
1 2 3 4 5 6 |
template<class Head, class... Tail> class TinyTuple<Head, Tail...> { Head head_; TinyTuple<Tail...> tail_; }; |
- 递归终点
- 是一个空类型
|
1 |
TinyTuple<> |
- 但一个独立的空对象通常不能完全没有大小
- 这是因为两个不同对象通常需要能拥有不同地址
|
1 2 3 |
struct Empty {}; static_assert(sizeof(Empty) >= 1); |
- 因此
empty可能占用额外空间或引起对齐填充
|
1 2 3 4 |
struct Data { int value; Empty empty; }; |
TinyTuple最深处也包含一个空的TinyTuple<>成员,因而可能造成空间浪费
EBO:空基类优化
- 如果空类型作为基类存在,编译器通常可以不给它分配额外空间
Empty Base Optimization,空基类优化,简称EBO或EBCO
|
1 2 3 4 5 6 7 8 9 10 |
struct Empty {}; struct MemberStorage { Empty empty; int value; }; struct BaseStorage : private Empty { int value; }; |
|
1 2 3 |
// 在常见实现中 sizeof(BaseStorage) // 可能等于一个 int 的大小,而 MemberStorage 可能更大 |
- 但不要写这样的可移植断言
EBO是否发生以及具体布局仍然与编译器、ABI和类型关系有关
|
1 |
static_assert(sizeof(BaseStorage) == sizeof(int)); // 不保证 |
让递归 tuple 继承 tail
|
1 2 3 4 |
class Tuple<Head, Tail...> { Head head_; Tuple<Tail...> tail_; }; |
- 可以改为
|
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 |
template<class... Types> class CompressedTuple; template<> class CompressedTuple<> { }; template<class Head, class... Tail> class CompressedTuple<Head, Tail...> : private CompressedTuple<Tail...> { private: using Base = CompressedTuple<Tail...>; Head head_; public: Head& head() noexcept { return head_; } Base& tail() noexcept { return *this; } }; |
- 递归终点
CompressedTuple<>现在成为空基类,而不是空成员- 编译器可以对它应用
EBO
- 编译器可以对它应用
- 但是这仍然只优化了空的递归尾节点,没有完整解决空元素的问题
- 如果
Empty仍以普通成员保存,它仍然可能占空间
- 如果
|
1 |
std::tuple<Empty, int> |
标准库风格:带索引的存储叶子
- 一种更接近工业实现的模型是
|
1 2 3 4 5 6 |
template<std::size_t I, class T> class TupleLeaf { private: T value_; }; |
- 然后让
tuple同时继承所有叶子
|
1 2 3 |
TupleImpl<0, int> TupleImpl<1, std::string> TupleImpl<2, 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 <cstddef> #include <utility> template<std::size_t I, class T> struct TupleLeaf { T value; }; template<class IndexSequence, class... Types> struct TupleImpl; template<std::size_t... I, class... Types> struct TupleImpl<std::index_sequence<I...>, Types...> : TupleLeaf<I, Types>... { }; template<class... Types> struct FlatTuple : TupleImpl<std::index_sequence_for<Types...>, Types...> { }; |
|
1 2 3 4 5 6 7 |
FlatTuple<int, std::string, double> // 相当于 TupleLeaf<0, int> TupleLeaf<1, std::string> TupleLeaf<2, double> |
- 考虑
|
1 2 3 4 5 6 7 8 9 10 11 |
FlatTuple<int, int> // 它需要两个不同基类 TupleLeaf<0, int> TupleLeaf<1, int> // 如果没有索引 TupleLeaf<int> TupleLeaf<int> // 就会重复继承完全相同的基类,访问和对象模型都会出现问题 |
- 所以大型模板实现中的索引经常不仅用于查找,还用于:
- 给相同类型的不同位置创建唯一身份
对空元素应用 EBO
- 在
C++20之前,常见做法是根据类型是否为空,选择成员存储或继承存储
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <type_traits> template< std::size_t I, class T, bool UseEbo = std::is_empty_v<T> && !std::is_final_v<T> > class TupleLeaf; template<std::size_t I, class T> class TupleLeaf<I, T, false> { private: T value_; }; template<std::size_t I, class T> class TupleLeaf<I, T, true> : private T { }; |
- 为什么要检查
!std::is_final_v<T>- 因为
final类型不能被继承 - 因此,即使它是空类型,也只能采用成员存储
- 因为
- 真实标准库实现还要处理
- 构造函数和约束
- 引用类型
const和右值- 异常规范
allocator-aware construction- 不同类型之间的转换构造
- 比较运算
ABI兼容性
C++20:[[no_unique_address]]
C++20提供了更直接的成员级空间优化机制
|
1 2 3 4 5 |
template<std::size_t I, class T> struct TupleLeaf { [[no_unique_address]] T value; }; |
- 它告诉编译器
- 如果对象模型允许,该成员不必拥有独占的存储地址
|
1 2 3 4 5 6 7 8 |
// 空 allocator 可能不会增加 Buffer 的大小 struct EmptyAllocator {}; struct Buffer { [[no_unique_address]] EmptyAllocator allocator; char* data; std::size_t size; }; |
- 这项特性在以下场景很常见
allocatordeleterpolicy objectcomparatorhash functorstateless callbacktuple-like storage
- 但需要注意
- 它是允许优化,不是保证特定大小
- 对齐和
ABI仍可能影响结果 - 重复的相同空类型不一定都能重叠
- 不应该根据这种对象布局设计网络协议或磁盘格式
递归 tuple 与索引叶子 tuple
| 特性 | 递归 | 索引叶子继承 |
| 理解难度 | 低 | 较高 |
get<I> |
递归进入 tail |
定位唯一叶子 |
| 重复类型 | 可以保存 | 通过索引获得唯一身份 |
| 空类型优化 | 需要额外处理 | 容易按叶子应用 EBO |
| 模板实例化深度 | 随索引递归 | 通常更扁平 |
| 工业实现价值 | 教学为主 | 更接近标准库实现 |
- 不同标准库的真实实现并不要求采用完全相同的内部结构。不能依赖:
sizeof(std::tuple<...>)- 或元素在内存中的排列顺序
make_tuple、tie 与 forward_as_tuple
std::make_tuplestd::make_tuple对std::reference_wrapper<T>有特殊处理,结果元素会成为T&
|
1 2 3 4 |
int id = 42; std::string name = "login"; auto t = std::make_tuple(id, name); |
|
1 2 3 4 5 6 7 |
std::tuple<int, std::string> // 它通常保存衰变后的值,相当于对参数应用类似 std::decay 的处理 // 去除引用 // 去除顶层 const、volatile // 数组退化为指针 // 函数退化为函数指针 |
|
1 2 3 4 5 |
// 因此,修改 tuple 中的元素通常不会修改原变量 std::get<0>(t) = 100; std::cout << id; // 仍然是 42 |
|
1 2 3 4 5 6 |
// 如果确实需要引用,可以使用 auto t = std::make_tuple<std::ref(id)); std::get<0>(t) = 100; // id现在是100 |
std::tiestd::tie创建左值引用的tupletie不拥有对象。被引用对象必须继续存活
|
1 2 3 4 5 6 7 |
int id; std::string name; auto refs = std::tie(id, name); // 类型大概为 std::tuple<int&, std::string&> |
|
1 2 3 4 5 6 7 8 9 10 |
// 它常用于解包 std::tie(id, name) = std::make_tuple(43, std::string{"login"}); // 还可以忽略某个元素 int id; double cost; std::tie(id, std::ignore, cost) = std::make_tuple(42, std::string{"login"}, 3.5); |
std::forward_as_tuple- 它创建由万能引用组成的
tuple - 它的用途是暂时保存参数的值类别,以便稍后继续完美转发
forward_as_tuple适合在同一个完整表达式或严格受控的同步调用链中转发,不适合保存到异步任务、容器或成员变量中
- 它创建由万能引用组成的
|
1 2 3 4 5 6 |
template<class... Args> void dispatch(Args&&... args) { auto forwarded = std::forward_as_tuple(std::forward<Args>(args)...); } |
|
1 2 3 4 5 |
// 但它不延长临时对象生命周期 auto t = std::forward_as_tuple(std::string{"temporary"}); // 语句结束后,临时 std::string 已经销毁,而 t 中仍然保存着指向它的右值引用 // 后续 std::get<0>(t) // 将产生悬空引用和未定义行为 |
三者的选择表
| 工具 | 保存内容 | 拥有对象吗 | 典型用途 |
make_tuple(args...) |
衰变后的值 | 是 | 打包并保存参数 |
tie(args...) |
左值引用 | 否 | 解包、引用已有变量 |
forward_as_tuple(args...) |
万能引用 | 否 | 临时保留值类别并继续转发 |
tuple{args...} |
CTAD推导出的元素 |
通常是值 | 直接构造 tuple |
结构化绑定
C++17可以使用结构化绑定
|
1 2 3 4 5 |
std::tuple<int, std::string> result{ 42, "login" }; auto [id, name] = result; |
- 这里的
auto会创建一个隐藏的 tuple 副本,id、name对应这个副本的元素
|
1 2 3 4 |
// 修改它们不会修改原 tuple id = 100; std::cout << std::get<0>(result); // 42 |
- 如果希望绑定原对象
|
1 2 3 4 |
auto& [id, name] = result; id = 100; // result 的第 0 个元素也变成 100 |
- 常见形式
|
1 2 3 4 |
auto [a, b] = t; // 对隐藏对象进行值初始化,通常发生复制 auto& [a, b] = t; // 左值引用 const auto& [a, b] = t; // const 左值引用 auto&& [a, b] = expr; // 根据 expr 保留值类别 |
std::apply 的原理
- 假设有:
|
1 2 3 |
void handle(int id, const std::string& command, double cost); |
- 参数保存在
tuple中
|
1 2 3 4 5 |
auto args = std::make_tuple( 42, std::string{"login"}, 3.5 ); |
- 使用
|
1 2 3 4 5 6 7 8 |
std::apply(handle, args); // 相当于 handle( std::get<0>(args), std::get<1>(args), std::get<2>(args) ); |
简化版 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 26 27 28 29 30 31 32 |
#include <functional> #include <tuple> #include <type_traits> #include <utility> template<class F, class Tuple, std::size_t... I> constexpr decltype(auto) tiny_apply_impl(F&& function, Tuple&& tuple, std::index_sequence<I...>) { return std::invoke( std::forward<F>(function), std::get<I>(std::forward<Tuple>(tuple))... ); } template<class F, class Tuple> constexpr decltype(auto) tiny_apply(F&& function, Tuple&& tuple) { using TupleType = std::remove_reference_t<Tuple>; constexpr std::size_t size = std::tuple_size_v<TupleType>; return tiny_apply_impl( std::forward<F>(function), std::forward<Tuple>(tuple), std::make_index_sequence<size>{} ); } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 如果 tuple 有三个元素 std::make_index_sequence<3> // 产生的类型是 std::index_sequence<0, 1, 2> // 于是 std::get<I>(tuple)... // 展开成 std::get<0>(tuple), std::get<1>(tuple), std::get<2>(tuple) // 完整调用最终变成 std::invoke( function, std::get<0>(tuple), std::get<1>(tuple), std::get<2>(tuple) ); |
为什么使用 std::invoke
- 如果只写
- 普通函数、
lambda和函数对象通常可以工作 - 但成员函数指针需要特殊语法
- 普通函数、
|
1 |
funtion(args...); |
std::invoke统一支持- 普通函数
lambda- 函数对象
- 成员函数指针
- 数据成员指针
std::reference_wrapper
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
struct Connection { void send(int type, std::string payload) { } }; Connection connection; auto args = std::make_tuple(1, std::string{"hello"}); std::apply( [&](int type, const std::string& payload) { connection.send(type, payload); }, args ); // 或者直接处理成员函数指针以及对象参数 auto call_args = std::make_tuple(&connection, 1, std::string{"hello"}); std::apply(&Connection::send, call_args); |
std::apply 的值类别传播
- 观察
|
1 |
std::get<I>(std::forward<Tuple>(tuple)) |
- 如果传给
apply的 tuple 是左值,元素通常作为左值传递
|
1 |
std::apply(function, tuple); |
- 如果
tuple是右值- 元素可以作为右值继续传递
|
1 |
std::apply(function, std::move(tuple)); |
|
1 2 3 4 5 6 7 8 |
auto consume = [](std::string value) { // 获取 string 所有权 }; std::tuple<std::string> t{"payload"}; // 这里可以将 t 中的字符串移动给 consume std::apply(consume, std::move(t)); |
网络项目中的例子
- 假设解析网络包头后得到两个字段
|
1 2 3 4 5 6 7 8 9 10 |
using HeaderResult = std::tuple<std::uint16_t, std::uint32_t>; HeaderResult decode_header(/* bytes */) { std::uint16_t command = 10; std::uint32_t payload_size = 1024; return {command, payload_size}; } |
|
1 2 3 4 5 6 7 8 9 |
// 调用方可以写 auto [command, payload_size] = decode_header(); // 这种局部、简单的返回值使用 tuple 是合理的 // 但不能这样做 HeaderResult header; // 错误思想:把 tuple 的内存直接发送出去 send(socket, &header, sizeof(header), 0); |
- 原因包括:
tuple的内存布局没有协议保证- 元素顺序不等于物理内存顺序
- 可能存在对齐和填充
- 不同标准库实现可能不同
- 不同编译器、
ABI和版本可能不同 - 整数还有网络字节序问题
异步代码中的危险 tuple
- 下面这种
tuple本身不拥有底层对象
|
1 |
std::tuple<Socket&, std::span<const std::byte>> task; |
- 如果保存到异步队列,必须确认
ocket在任务执行前不会销毁span指向的缓冲区仍然存活- 缓冲区不会被并发修改
socket的关闭与任务取消有同步保证
- 问题不在
tuple本身,而在于tuple很容易把多个非拥有引用打包起来,让生命周期关系变得不明显 - 工程上常用的判断方法是
- 这个
tuple被保存后,其中每一个元素究竟拥有资源,还是仅仅借用资源
- 这个
|
1 2 3 4 5 6 |
// 如果任务跨线程、跨事件循环或延迟执行,通常优先保存拥有所有权的类型 std::tuple< std::shared_ptr<Connection>, std::vector<std::byte> > |
其他
阅读模板的翻译方法
- 下面模板通常意味着:
- 给参数包中的每个元素编号
- 支持重复类型
- 根据索引选择某个元素
- 可能进行
EBO或[[no_unique_address]]优化
|
1 2 |
template<std::size_t I, class T> struct Leaf; |
- 下面模板通常意味着:
- 将
tuple、数组或参数包按索引展开 - 生成
get<I>(...)... - 并行处理两个或多个参数包
- 将运行形式转换成编译期展开形式
- 将
|
1 |
std::index_sequence<I...> |
- 下面模板通常意味着:
- 代码准备处理
tuple-like对象 - 后面很可能会构造
make_index_sequence - 随后用
get<I>展开元素
- 代码准备处理
|
1 |
std::tuple_size_v<std::remove_reference_t<T>> |
- 对下面,要检查:
- 元素是否应该被移动
tuple后续是否还会使用- 元素中是否含有引用
f的参数是按值还是按引用接收
|
1 |
std::apply(f, std::move(tuple)) |
- 对下面,第一反应应该是检查临时对象和异步生命周期
|
1 |
std::forward_as_tuple(...) |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Soui一03/17
- ♥ gflags记述:记录101/12
- ♥ C++_volatile10/08
- ♥ C++_关于函数调用过程10/30
- ♥ 深入理解C++11:C++11新特性解析与应用 三01/05
- ♥ Soui二05/18
