创作于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