在C++中创建具名元组(namedtuple)

创作于zhihu

首先不建议使用std::tupletuple是给模板,而不是正常代码用的

泛型中,有时我们不得不根据变长的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 

表示当前遍历下标为Ituple中下标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