创作于zhihu
首先不建议使用std::tuple
,tuple
是给模板,而不是正常代码用的
泛型中,有时我们不得不根据变长的typename
保存对象(std::tuple<type_trait<T>...>
)
因为我们没法直接在模板类里根据变长的typename
存储变长的不同类型成员..
比如像实现一个这样回滚的功能
template <size_t I, typename Tup, typename Lhs, typename ...Oth>
void impl(Tup const & t, Lhs & lhs, Oth & ...oth) {
lhs = std::get<I>(t);
if constexpr (sizeof...(oth) > 0) impl<I + 1, Tup, Oth...>(t, oth...);
};
auto snapshot = [](auto &...arg) {
return [&, rhs = std::tuple<std::remove_reference_t<decltype(arg)>...>{
arg...}]() { impl<0>(rhs, arg...); };
};
int main() {
int a = 1, b = 2; string c = "2.33";
auto rollback = snapshot(a, b, c);
try {
a = b = 3; c = "1";
throw 0;
} catch (...) {
rollback();
}
cout << a << ", " << b << ", " << c;
//1, 2, 2.33
}
但在其他场景下,我们应该首先使用聚合体替代tuple
,因为tuple
一般通过多继承或递归继承实现,引入了额外开销
其次tuple
没有可读性,如果我需要匿名类型,我会先质疑自己是不是在写屎山..
而且C++ 17
中,我们可以对struct
使用结构化绑定了
struct Person {short age; int id; string name;}
auto& [a, i, n] = Person{17, 32, "bob"};
如果我们真的需要拿tuple
来存数据的话(比如让类型兼容std::apply
或其他模板),也至少要实现具名元组(namedtuple
)的机制,因为对tuple
取单个成员时,硬编码的std::get<0>
,std::get<1>
... 很脆弱,匿名字段让代码可维护性变得很差
首先我们可以使用enum
变量名来作为namedtuple
中的字段名
先定义一个模板类
template <typename T> struct Named;
特化一个Named
,存储对应tuple
类型与枚举
struct Person;
template <> struct Named<Person> {
enum { name = 0, address, id, age };
using Tuple = tuple<string, string, int, short>;
};
其中enum
是匿名弱枚举,枚举变量名会上升到Named<Person>
之后可以这样使用
int main() {
Named<Person>::Tuple t{"alice", "CN", 114514, 17};
cout << get<Named<Person>::name>(t) << endl;
cout << get<Named<Person>::address>(t) << endl;
cout << get<Named<Person>::age>(t) << endl;
}
但是这样的话,enum
里的名字和存储类型就分裂开了,也就是使用中不能方便地查看某个名字对应什么类型,可能看差眼了
这种写法不够优雅,除了拿变量名当名字,我们其实所以可以把另一个类型名作为具名元组中的字段名
然后使用std::pair
绑定字段名和字段类型
这样通过简单的元编程,我们就可以实现一个比较好维护的namedtuple
,写法如下
namespace MyTypeList {
template <typename U, typename V> using _ = pair<U, V>;
struct name; struct address; struct age; struct id; struct master;
using Person = DefineNamedTuple<_<name, string>, _<address, string>, _<id, int>, _<age, short>>;
using Dog = DefineNamedTuple<_<master, Person::Tuple>, _<name, string>>;
}; // namespace MyTypeList
因为使用类型名作为名字,所以我们要使用一个namespace
封住这些名字防止污染代码中其他类
核心是实现DefineNamedTuple<pair<Name, Type>...>
template <typename... NameAndTypes> struct DefineNamedTuple {
using Tuple = std::tuple<typename UnPackPair<NameAndTypes>::second...>;
template <typename T> struct Index_ {
template <std::size_t I, typename U, typename... V> struct Helper {
constexpr inline static std::size_t _v =
std::is_same_v<T, typename UnPackPair<U>::first>
? I
: Helper<I + 1, V...>::_v;
};
template <std::size_t I, typename U> struct Helper<I, U> {
constexpr inline static std::size_t _v =
std::is_same_v<T, typename UnPackPair<U>::first> ? I
: I + 1;
};
constexpr inline static std::size_t _v = Helper<0, NameAndTypes...>::_v;
};
template <typename T> constexpr inline static std::size_t At = Index_<T>::_v;
};
这个模板类接收变长的NameAndTypes
(pair
),首先定义了Tuple
别名,解构出pair
中存储的实际值类型,然后得到实际tuple
类型
其次内部有一个模板变量At<T>
让编译器计算名字T
对应的tuple
下标
核心是内部的Helper
递归模板
template <std::size_t I, typename U, typename... V> struct Helper
表示当前遍历下标为I
,tuple
中下标I
对应的名字是U::first
,下标I
以后的名字是V::first...
std::is_same_v<T, typename UnPackPair<U>::first>
? I
: Helper<I + 1, V...>::_v;
所以如果当前名字是T
,返回I
,否则++I
,在之后的名字V::first...
中查找
template <std::size_t I, typename U> struct Helper<I, U> {
constexpr inline static std::size_t _v =
std::is_same_v<T, typename UnPackPair<U>::first>
? I
: I + 1;
};
再特化一个Helper
模板表示递归边界(查找到了最后一个名字U::first
)
其中的UnpackPair<U, V>
用于解构pair
:
template <typename U> struct UnPackPair;
template <typename U, typename V> struct UnPackPair<pair<U, V>> {
using first = U;
using second = V;
};
使用举栗:
int main() {
using namespace MyTypeList;
Person::Tuple t{"alice", "CN", 114514, 17};
Dog::Tuple t2{t, "big black"};
cout << get<Person::At<name>>(t) << endl;
cout << get<Person::At<address>>(t) << endl;
cout << get<Person::At<age>>(t) << endl;
cout << get<Person::At<id>>(t) << endl;
cout << get<Dog::At<name>>(t2) << endl;
cout << get<Person::At<id>>(get<Dog::At<master>>(t2)) << endl;
}
// alice
// CN
// 17
// 114514
// big black
// 114514
完整实现:Gist