【问题标题】:Determine member offset of struct or tuple in template确定模板中结构或元组的成员偏移量
【发布时间】:2019-03-08 22:05:11
【问题描述】:

我想编写一个将表格写入 HDF5 文件的模板函数。 签名应该类似于

template<typename record> void writeTable(const std::vector<record>& data);

其中记录是一个结构,或者

template<typename... elements> 
    void writeTable(const std::vector<std::tuple<elements...>>& data);

实际的实现会有更多的参数来确定目标等。

要写入数据,我需要定义一个 HDF5 复合类型,其中包含成员的名称和偏移量。通常你会使用HOFFSET 宏来获取字段偏移量,但由于我事先不知道结构字段,所以我不能这样做。

到目前为止,我尝试的是从 typename 包构造一个结构类型。天真的实现没有标准布局,但实现here 有。剩下的就是获取成员的偏移量。我想将参数包扩展为带有偏移量的初始化列表:

#include <vector>

template<typename... members> struct record {};

template<typename member, typename... members> struct record<member, members...> : 
    record<members...> {
  record(member m, members... ms) : record<members...>(ms...), tail(m) {}
  member tail;
};

template<typename... Args> void 
    make_table(const std::string& name, const std::vector<record<Args...>>& data) {
  using record_type = record<Args...>;
  std::vector<size_t> offsets = { get_offset(record_type,Args)... };
}

int main() {
  std::vector<record<int, float>> table = { {1, 1.0}, {2, 2.0} };
  make_table("table", table);
}

get_offset 是否有可能的实现?我认为不会,因为在record&lt;int, int&gt; 的情况下会模棱两可。还有其他方法吗?

或者有没有其他方法可以解决这个问题?

【问题讨论】:

标签: c++ templates variadic-templates template-meta-programming


【解决方案1】:

计算偏移量非常简单。给定一个类型为 T0、T1 ... TN 的元组。 T0 的偏移量是0(只要你在char 数组上使用alignas(T0)T1 的偏移量是sizeof(T0) 向上舍入到alignof(T1)

一般来说,TB(在TA之后)的偏移量是round_up(offset_of&lt;TA&gt;() + sizeof(TA), alignof(TB))

计算std::tuple 中元素的偏移量可以这样完成:

constexpr size_t roundup(size_t num, size_t multiple) {
  const size_t mod = num % multiple;
  return mod == 0 ? num : num + multiple - mod;
}

template <size_t I, typename Tuple>
struct offset_of {
  static constexpr size_t value = roundup(
    offset_of<I - 1, Tuple>::value + sizeof(std::tuple_element_t<I - 1, Tuple>),
    alignof(std::tuple_element_t<I, Tuple>)
  );
};

template <typename Tuple>
struct offset_of<0, Tuple> {
  static constexpr size_t value = 0;
};

template <size_t I, typename Tuple>
constexpr size_t offset_of_v = offset_of<I, Tuple>::value;

这是一个测试套件。从第一个测试中可以看出,考虑了元素的对齐方式。

static_assert(offset_of_v<1, std::tuple<char, long double>> == 16);
static_assert(offset_of_v<2, std::tuple<char, char, long double>> == 16);
static_assert(offset_of_v<3, std::tuple<char, char, char, long double>> == 16);
static_assert(offset_of_v<4, std::tuple<char, char, char, char, long double>> == 16);

static_assert(offset_of_v<0, std::tuple<int, double, int, char, short, long double>> == 0);
static_assert(offset_of_v<1, std::tuple<int, double, int, char, short, long double>> == 8);
static_assert(offset_of_v<2, std::tuple<int, double, int, char, short, long double>> == 16);
static_assert(offset_of_v<3, std::tuple<int, double, int, char, short, long double>> == 20);
static_assert(offset_of_v<4, std::tuple<int, double, int, char, short, long double>> == 22);
static_assert(offset_of_v<5, std::tuple<int, double, int, char, short, long double>> == 32);

我在上述测试中硬编码了偏移量。如果以下测试成功,则偏移量是正确的。

static_assert(sizeof(char) == 1 && alignof(char) == 1);
static_assert(sizeof(short) == 2 && alignof(short) == 2);
static_assert(sizeof(int) == 4 && alignof(int) == 4);
static_assert(sizeof(double) == 8 && alignof(double) == 8);
static_assert(sizeof(long double) == 16 && alignof(long double) == 16);

std::tuple 似乎按顺序存储它的元素(没有对它们进行排序以优化填充)。以下测试证明了这一点。我不认为标准要求std::tuple 以这种方式实现,所以我认为以下测试不能保证成功。

template <size_t I, typename Tuple>
size_t real_offset(const Tuple &tup) {
  const char *base = reinterpret_cast<const char *>(&tup);
  return reinterpret_cast<const char *>(&std::get<I>(tup)) - base;
}

int main(int argc, char **argv) {
  using Tuple = std::tuple<int, double, int, char, short, long double>;
  Tuple tup;
  assert((offset_of_v<0, Tuple> == real_offset<0>(tup)));
  assert((offset_of_v<1, Tuple> == real_offset<1>(tup)));
  assert((offset_of_v<2, Tuple> == real_offset<2>(tup)));
  assert((offset_of_v<3, Tuple> == real_offset<3>(tup)));
  assert((offset_of_v<4, Tuple> == real_offset<4>(tup)));
  assert((offset_of_v<5, Tuple> == real_offset<5>(tup)));
}

现在我已经完成了所有这些工作,real_offset 函数是否适合您的需求?


这是一个使用offset_of 访问char[] 的元组的最小实现。由于reinterpret_cast,这是未定义的行为。即使我以相同的字节构造对象并以相同的字节访问对象,它仍然是 UB。请参阅this answer 了解所有标准。它适用于您可以找到的每个编译器,但它是 UB,所以无论如何都要使用它。这个元组是标准布局(不像std::tuple)。如果您的元组的元素都是可简单复制的,您可以删除复制并移动构造函数并将它们替换为memcpy

template <typename... Elems>
class tuple;

template <size_t I, typename Tuple>
struct tuple_element;

template <size_t I, typename... Elems>
struct tuple_element<I, tuple<Elems...>> {
  using type = std::tuple_element_t<I, std::tuple<Elems...>>;
};

template <size_t I, typename Tuple>
using tuple_element_t = typename tuple_element<I, Tuple>::type;

template <typename Tuple>
struct tuple_size;

template <typename... Elems>
struct tuple_size<tuple<Elems...>> {
  static constexpr size_t value = sizeof...(Elems);
};

template <typename Tuple>
constexpr size_t tuple_size_v = tuple_size<Tuple>::value;

constexpr size_t roundup(size_t num, size_t multiple) {
  const size_t mod = num % multiple;
  return mod == 0 ? num : num + multiple - mod;
}

template <size_t I, typename Tuple>
struct offset_of {
  static constexpr size_t value = roundup(
    offset_of<I - 1, Tuple>::value + sizeof(tuple_element_t<I - 1, Tuple>),
    alignof(tuple_element_t<I, Tuple>)
  );
};

template <typename Tuple>
struct offset_of<0, Tuple> {
  static constexpr size_t value = 0;
};

template <size_t I, typename Tuple>
constexpr size_t offset_of_v = offset_of<I, Tuple>::value;

template <size_t I, typename Tuple>
auto &get(Tuple &tuple) noexcept {
  return *reinterpret_cast<tuple_element_t<I, Tuple> *>(tuple.template addr<I>());
}

template <size_t I, typename Tuple>
const auto &get(const Tuple &tuple) noexcept {
  return *reinterpret_cast<tuple_element_t<I, Tuple> *>(tuple.template addr<I>());
}

template <typename... Elems>
class tuple {
  alignas(tuple_element_t<0, tuple>) char storage[offset_of_v<sizeof...(Elems), tuple<Elems..., char>>];
  using idx_seq = std::make_index_sequence<sizeof...(Elems)>;

  template <size_t I>
  void *addr() {
    return static_cast<void *>(&storage + offset_of_v<I, tuple>);
  }

  template <size_t I, typename Tuple>
  friend auto &get(const Tuple &) noexcept;

  template <size_t I, typename Tuple>
  friend const auto &get(Tuple &) noexcept;

  template <size_t... I>
  void default_construct(std::index_sequence<I...>) {
    (new (addr<I>()) Elems{}, ...);
  }
  template <size_t... I>
  void destroy(std::index_sequence<I...>) {
    (get<I>(*this).~Elems(), ...);
  }
  template <size_t... I>
  void move_construct(tuple &&other, std::index_sequence<I...>) {
    (new (addr<I>()) Elems{std::move(get<I>(other))}, ...);
  }
  template <size_t... I>
  void copy_construct(const tuple &other, std::index_sequence<I...>) {
    (new (addr<I>()) Elems{get<I>(other)}, ...);
  }
  template <size_t... I>
  void move_assign(tuple &&other, std::index_sequence<I...>) {
    (static_cast<void>(get<I>(*this) = std::move(get<I>(other))), ...);
  }
  template <size_t... I>
  void copy_assign(const tuple &other, std::index_sequence<I...>) {
    (static_cast<void>(get<I>(*this) = get<I>(other)), ...);
  }

public:
  tuple() noexcept((std::is_nothrow_default_constructible_v<Elems> && ...)) {
    default_construct(idx_seq{});
  }
  ~tuple() {
    destroy(idx_seq{});
  }
  tuple(tuple &&other) noexcept((std::is_nothrow_move_constructible_v<Elems> && ...)) {
    move_construct(other, idx_seq{});
  }
  tuple(const tuple &other) noexcept((std::is_nothrow_copy_constructible_v<Elems> && ...)) {
    copy_construct(other, idx_seq{});
  }
  tuple &operator=(tuple &&other) noexcept((std::is_nothrow_move_assignable_v<Elems> && ...)) {
    move_assign(other, idx_seq{});
    return *this;
  }
  tuple &operator=(const tuple &other) noexcept((std::is_nothrow_copy_assignable_v<Elems> && ...)) {
    copy_assign(other, idx_seq{});
    return *this;
  }
};

或者,您可以使用此功能:

template <size_t I, typename Tuple>
size_t member_offset() {
  return reinterpret_cast<size_t>(&std::get<I>(*static_cast<Tuple *>(nullptr)));
}

template <typename Member, typename Class>
size_t member_offset(Member (Class::*ptr)) {
  return reinterpret_cast<size_t>(&(static_cast<Class *>(nullptr)->*ptr));
}

template <auto MemPtr>
size_t member_offset() {
  return member_offset(MemPtr);
}

再一次,这是未定义的行为(因为nullptr 取消引用和reinterpret_cast),但它会在每个主要编译器中按预期工作。该函数不能是constexpr(即使成员偏移量是编译时计算的)。

【讨论】:

  • 这看起来很不错。 real_offset 中的计算总是正确的吗?即使元组没有标准布局?
  • @maufl real_offset 中的计算总是正确的并且适用于任何类型(不仅仅是元组)。但是,它使用reinterpret_cast,因此它在技术上是未定义的行为,并且不能在constexpr 上下文中使用(即使它是编译时计算)。 offset_of 内部的计算仅适用于标准布局类型。 std::tuple 是标准布局,因此适用于所有元组。标准布局类型仍然是标准布局,即使它包含非标准布局成员。我建议使用offset_of,因为它不是未定义的行为。
  • 谢谢。我检查了但在 gcc 中,std::tuple 没有标准布局。未定义的行为让我感到害怕,以及为什么我还没有选择实现。
  • @maufl 我刚刚检查过 clang,你是对的。该死!
  • @maufl 也许您可以找到一种实现标准布局的元组的方法。一个想法是存储一个字节数组并使用 offset_of 和placement new。那将是标准布局。
【解决方案2】:

不确定您到底想要什么,但是...如何使用基于索引序列的递归(从 C++14 开始)如下?

#include <vector>
#include <utility>
#include <iostream>

template <typename... members>
struct record
 { };

template <typename member, typename... members>
struct record<member, members...> : record<members...>
 {
   record (member m, members... ms) : record<members...>(ms...), tail(m)
    { }

   member tail;
 };

template <std::size_t, typename, std::size_t = 0u>
struct get_offset;

template <std::size_t N, typename A0, typename ... As, std::size_t Off>
struct get_offset<N, record<A0, As...>, Off> 
   : public get_offset<N-1u, record<As...>, Off+sizeof(A0)>
 { };

template <typename A0, typename ... As, std::size_t Off>
struct get_offset<0u, record<A0, As...>, Off> 
   : public std::integral_constant<std::size_t, Off>
 { };

template <typename... Args, std::size_t ... Is>
auto make_table_helper (std::string const & name,
                        std::vector<record<Args...>> const & data,
                        std::index_sequence<Is...> const &)
 { return std::vector<std::size_t>{ get_offset<Is, record<Args...>>::value... }; }

template <typename... Args>
auto make_table (std::string const & name,
                 std::vector<record<Args...>> const & data)
 { return make_table_helper(name, data, std::index_sequence_for<Args...>{}); }

int main ()
 {
   std::vector<record<int, float>> table = { {1, 1.0}, {2, 2.0} };

   auto v = make_table("table", table);

   for ( auto const & o : v )
      std::cout << o << ' ';

   std::cout << std::endl;
 }

很遗憾,这不是一个有效的解决方案,因为最后一个值被计算了 n 次。

【讨论】:

  • 这不考虑对齐吗?如果该类型没有标准布局,是否有任何偏移量的保证?
  • @maufl - 对;只是为了给出递归部分的想法;对于对齐,而不是Off+sizeof(A0),我想您可以使用Off+roundup(A0, alignof(A0)) (参见Kerndog73 答案中的roundup()),但同样在这种情况下,我不确定您是否可以得到保证.
猜你喜欢
  • 2023-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多