如何在C++模板中获取结构体/元组成员偏移量以实现HDF5表写入?
Great question! You're right that using just the member type to get offsets won't work when there are duplicate types (like record<int, int>) because the compiler can't disambiguate which member you're referring to. Let's break down some practical solutions to this problem, including fixes for your custom record struct and alternative approaches that might simplify your code.
Solution 1: Fixing Your Custom record Struct with Index-Based Offsets
The core issue with your current approach is relying on type alone to identify members. Instead, we can use compile-time indices to target each member, which avoids ambiguity even when types repeat. Here's how to adjust your code:
Step 1: Add Base Type Alias to record
First, update your record struct to include an alias for its base class, which will let us recursively traverse the inheritance hierarchy:
#include <vector> #include <utility> // for index_sequence // Empty base case template<typename... members> struct record { using base_type = void; }; // Specialization for non-empty member list template<typename member, typename... members> struct record<member, members...> : record<members...> { using base_type = record<members...>; record(member m, members... ms) : record<members...>(ms...), tail(m) {} member tail; };
Step 2: Implement Index-Based get_offset
Next, write a constexpr function that uses indices to fetch the offset of each member. We'll recursively climb the inheritance hierarchy until we reach the target index:
template<size_t I, typename T> constexpr size_t get_offset() { static_assert(I == 0 || !std::is_same_v<typename T::base_type, void>, "Index out of bounds"); if constexpr (I == 0) { // Offset of the current struct's tail member return offsetof(T, tail); } else { // Recurse into the base class for the next index return get_offset<I-1, typename T::base_type>(); } }
Step 3: Expand Indices in make_table
Use std::index_sequence to generate compile-time indices for each member, then expand them to get all offsets:
template<typename... Args, size_t... Is> void make_table_impl(const std::string& name, const std::vector<record<Args...>>& data, std::index_sequence<Is...>) { using record_type = record<Args...>; constexpr size_t num_members = sizeof...(Args); // Note: Offsets are ordered to match the template parameter list. // If you want offsets to match the in-memory storage order (base class members first), // reverse the indices: num_members - 1 - Is instead of Is std::vector<size_t> offsets = { get_offset<Is, record_type>()... }; // Now you can use these offsets to build your HDF5 compound type } template<typename... Args> void make_table(const std::string& name, const std::vector<record<Args...>>& data) { make_table_impl(name, data, std::index_sequence_for<Args...>()); }
Key Note About Member Order
Your record struct stores base class members first (e.g., record<int, float> has the float member from the base class at offset 0, followed by the int member at offset sizeof(float)). If you want the offsets to match the template parameter order, use num_members - 1 - Is in the offsets initializer instead of Is.
Solution 2: Use std::tuple Instead of Custom record
Why reinvent the wheel? std::tuple is a standard-layout type (C++11+) and handles member storage efficiently. You can get member offsets using indices, no custom struct needed:
Implement get_tuple_offset
#include <tuple> template<size_t I, typename... Args> constexpr size_t get_tuple_offset() { constexpr std::tuple<Args...> t{}; constexpr const char* base_ptr = reinterpret_cast<const char*>(&t); constexpr const char* member_ptr = reinterpret_cast<const char*>(&std::get<I>(t)); return member_ptr - base_ptr; }
Update writeTable for std::tuple
template<typename... elements, size_t... Is> void writeTable_impl(const std::vector<std::tuple<elements...>>& data, const std::string& path, std::index_sequence<Is...>) { std::vector<size_t> offsets = { get_tuple_offset<Is, elements...>()... }; // Build HDF5 compound type using offsets and element types // Write data to HDF5 file } template<typename... elements> void writeTable(const std::vector<std::tuple<elements...>>& data, const std::string& path) { writeTable_impl(data, path, std::index_sequence_for<elements...>()); }
This is cleaner, uses standard library components, and avoids the inheritance hierarchy quirks of your custom record struct.
Solution 3: Add Named Fields (For HDF5 Member Names)
HDF5 compound types usually require member names, not just offsets. To handle this, you can create tagged field types that include both the member type and name:
Define Tagged Fields
// Define field tags with name and type struct FieldID { static constexpr const char* name = "id"; using type = int; }; struct FieldValue { static constexpr const char* name = "value"; using type = float; };
Create a Named Record Type
template<typename... Fields> struct NamedRecord { std::tuple<typename Fields::type...> data; // Forward constructor arguments to the tuple template<typename... Args> NamedRecord(Args&&... args) : data(std::forward<Args>(args)...) {} // Get compile-time list of field names static constexpr std::array<const char*, sizeof...(Fields)> get_names() { return { Fields::name... }; } };
Update writeTable for Named Records
template<typename... Fields, size_t... Is> void writeTable_impl(const std::vector<NamedRecord<Fields...>>& data, const std::string& path, std::index_sequence<Is...>) { using tuple_type = std::tuple<typename Fields::type...>; // Get offsets for each member std::vector<size_t> offsets = { get_tuple_offset<Is, typename Fields::type...>()... }; // Get names for each member std::vector<std::string> names = { Fields::name... }; // Build HDF5 compound type with names, types, and offsets // Write data to HDF5 } template<typename... Fields> void writeTable(const std::vector<NamedRecord<Fields...>>& data, const std::string& path) { writeTable_impl(data, path, std::index_sequence_for<Fields...>()); }
This approach gives you everything you need for HDF5: member types, offsets, and human-readable names—even if multiple members have the same type.
Final Thoughts
- If you want to stick with your custom
recordstruct, the index-based offset approach will fix the ambiguity issue. - Using
std::tuplesimplifies your code and leverages the standard library. - Adding named fields is the most practical for HDF5, since you'll need member names anyway.
All these solutions work in C++17 and later; if you're using an older standard, you can replace constexpr if with template specialization for the offset functions.
内容的提问来源于stack exchange,提问作者maufl

