Определить смещение члена структуры или кортежа в шаблоне - PullRequest
3 голосов
/ 09 марта 2019

Я хочу написать шаблонную функцию, которая записывает таблицы в файлы 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 для получения смещения поля, но поскольку я заранее не знаю полей структуры, я не могу этого сделать.

То, что я до сих пор пробовал, - это создание типа структуры изТиповое имя.Наивная реализация не имела стандартной компоновки, но реализация здесь имеет.Все, что осталось, это получить смещение участников.Я хотел бы расширить пакет параметров в список инициализаторов со смещениями:

#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<int, int> это было бы неоднозначно.Есть ли другой способ сделать это?

Или есть другой способ решить эту проблему?

Ответы [ 2 ]

1 голос
/ 09 марта 2019

Не уверены, что понимаете, что именно вам нужно, но ... как насчет использования рекурсии, основанной на последовательности индексов (начиная с 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 раз.

1 голос
/ 09 марта 2019

Расчет смещения довольно прост. Дан кортеж с типами T0, T1 ... TN. Смещение T0 равно 0 (при условии, что вы используете alignas(T0) в массиве char. Смещение T1 - это sizeof(T0), округленное до alignof(T1).

Как правило, смещение TB (после TA) составляет round_up(offset_of<TA>() + 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 подойдет вам?


Это минимальная реализация кортежа, который обращается к char[] с помощью offset_of. Это неопределенное поведение, хотя из-за reinterpret_cast. Несмотря на то, что я создаю объект в тех же байтах и ​​обращаюсь к нему в тех же байтах, это все равно UB. См. этот ответ для всех стандартных. Он будет работать на любом компиляторе, который вы можете найти, но это 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 (даже если смещение элемента является вычислением времени компиляции).

...