文章

使用 C++ template 构建一个通信用序列化反序列化模块

使用 C++ template 构建一个通信用序列化反序列化模块

1. 序列化 PacketSerializer

分为以下几个步骤:

  • 需要一个 buffer 来存储序列化过程中的数据,使用 std::vector 作为底层存储。
  • 提供一个模板实现的基础函数 writeToBuffer,将基本类型数据写入 buffer。
  • 提供一个模板函数 pack,支持将基础类型数据,以及其数组,以及 vector 类型进行序列化。
  • 在 pack 函数的基础上,提供一个模板函数 packMultiple,支持将多个数据打包进行序列化。
  • 提供接口函数 finalize,添加包头、包尾,组装成完整的协议,返回序列化后的数据 buffer。

1.1. writeToBuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  template <typename T>
  void writeToBuffer(const T& value) {
    static_assert(std::is_integral_v<T> || std::is_floating_point_v<T>, "Type must be arithmetic");

    const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&value);
    for (size_t i = 0; i < sizeof(T); i++) {
      buffer_.push_back(bytes[i]);
    }
  }

  template <typename T>
  void writeArrayToBuffer(const T* array, size_t count) {
    static_assert(std::is_integral_v<T> || std::is_floating_point_v<T>, "Array element type must be arithmetic");

    for (size_t i = 0; i < count; i++) {
      writeToBuffer(array[i]);
    }
  }

1.2. pack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  template <typename T>
  typename std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>, PacketSerializer&>  //
  pack(const T& value) {
    writeToBuffer(value);
    return *this;
  }

  template <typename T, size_t N>
  typename std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>, PacketSerializer&>  //
  pack(const T (&array)[N]) {
    writeArrayToBuffer(array, N);
    return *this;
  }

  template <typename T>
  typename std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>, PacketSerializer&>  //
  pack(const std::vector<T>& vec) {
    if (!vec.empty()) {
      writeArrayToBuffer(vec.data(), vec.size());
    }
    return *this;
  }

1.3. packMultiple

1
2
3
4
5
6
7
8
  template <typename T, typename... Args>
  PacketSerializer& packMultiple(const T& first, const Args&... args) {
    pack(first);
    if constexpr (sizeof...(args) > 0) {
      packMultiple(args...);
    }
    return *this;
  }

1.4. 打包原始结构体数据

提供另外一种类型打包接口,用于直接序列化 1 字节对齐的结构体数据。

1
2
3
4
5
6
  template <typename T>
  PacketSerializer& packRawData(const T& structData) {
    static_assert(std::is_trivially_copyable_v<T>, "Type must be trivially copyable for direct packing");
    const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&structData);
    return appendBytes(bytes, sizeof(T));
  }

1.5. finalize

组装成完成的数据包,并返回序列化后的 packet。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  std::vector<uint8_t> finalize() {
    std::vector<uint8_t> packet;

    uint16_t total_length = 2 + 2 + static_cast<uint16_t>(buffer_.size()) + 1;

    packet.push_back(static_cast<uint8_t>(frame_header_ & 0xFF));
    packet.push_back(static_cast<uint8_t>((frame_header_ >> 8) & 0xFF));

    packet.push_back(static_cast<uint8_t>(total_length & 0xFF));
    packet.push_back(static_cast<uint8_t>((total_length >> 8) & 0xFF));

    packet.insert(packet.end(), buffer_.begin(), buffer_.end());

    const uint8_t checksum = calculateChecksum(packet.data(), packet.size());
    packet.push_back(checksum);

    return packet;
  }

1.6. 便利函数

1
2
3
4
5
6
7
8
template <typename... Args>
std::vector<uint8_t> createPacket(uint16_t frame_header, const Args&... args) {
  PacketSerializer serializer(frame_header);
  if constexpr (sizeof...(args) > 0) {
    serializer.packMultiple(args...);
  }
  return serializer.finalize();
}
1
2
3
4
5
6
7
template <typename T>
std::vector<uint8_t> createRawDataPacket(uint16_t frame_header, const T& structData) {
  static_assert(std::is_trivially_copyable_v<T>, "Type must be trivially copyable for direct packing");
  PacketSerializer serializer(frame_header);
  serializer.packRawData(structData);
  return serializer.finalize();
}

2. 反序列化 PacketDeserializer

首先解出header。payload部分的解析,与序列化提供相同步骤的接口函数 unpack,unpackRawData。并提供便利接口:

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T, typename... Args>
bool unpackMultiple(PacketDeserializer& deserializer, T& first, Args&... args) {
  if (deserializer.unpack(first) == 0) {
    return false;
  }

  if constexpr (sizeof...(args) > 0) {
    return unpackMultiple(deserializer, args...);
  }

  return true;
}

实际应用中,由于存在一些运行时判断,实际在使用 PacketDeserializer 的时候,需要依据具体的协议格式,进行相应的逻辑处理,即针对这些特殊协议实现一个特化版本的解析封装。比如,某个字段的值决定后续字段的类型和数量等。

3. 提取结构体中的字段进行序列化

由于通信需要,通信中,通信双方需要设置以及接收参数结构体中的字段。梳理需求,提取出接口:

    1. 根据字段名称字符串(field name),提取结构体中的字段值:get_field_value_by_name,get_field_value_by_name_as。
    1. 根据结构体中的字段(field),获取字段的名称字符串(field name):get_field_name_by_field。
    1. 由于C++ 17 支持不完善,不能提取字段名称字符串,此处使用手动定义字段名-字段偏移地址-字段类型映射表。

定义需要的数据结构。定义支持的字段数据类型枚举 field_type_e,枚举对应的数据类型大小映射表 field_type_sizes,以及字段描述结构体 FieldInfo。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// clang-format off
using field_var_t =std::variant<int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t, float, double, std::string>;
enum field_type_e : uint8_t {
  field_type_i8,    field_type_ui8,   field_type_i16,   field_type_ui16,
  field_type_i32,   field_type_ui32,  field_type_i64,   field_type_ui64,
  field_type_f32,   field_type_f64,   field_type_cstrarr,field_type_cstrptr,
  field_type_count
};
constexpr std::array<size_t, field_type_count> field_type_sizes = {
    sizeof(int8_t),  sizeof(uint8_t),  sizeof(int16_t),  sizeof(uint16_t),
    sizeof(int32_t), sizeof(uint32_t), sizeof(int64_t),  sizeof(uint64_t),
    sizeof(float),   sizeof(double),   0, 0
};
// clang-format on

struct FieldInfo {
  std::string_view name;
  std::size_t offset;
  field_type_e type;
};

3.1. 根据输入的结构体实例,以及域字段(field),获取字段名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename StructType, typename FieldType, size_t FieldInfoN>
std::string get_field_name_by_field(const StructType& sinst, const FieldType& field,
                                    const std::array<FieldInfo, FieldInfoN>& fields) {
  const void* base = static_cast<const void*>(std::addressof(sinst));
  const void* field_addr = static_cast<const void*>(std::addressof(field));
  const ptrdiff_t offset = static_cast<const char*>(field_addr) - static_cast<const char*>(base);
  for (const auto& f : fields) {
    if (f.offset == offset) {
      return std::string(f.name);
    }
  }

  return {};
}

实现:根据结构体实例的地址,以及字段的地址,计算字段的偏移地址 offset。然后在手动定义的字段描述信息数组 fields 中查找该偏移地址对应的字段名称,并返回。

3.2. 根据字段名称(field name)获取结构体中该字段的值,返回一个 field_var_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
template <typename StructType, size_t FieldInfoN>
std::tuple<field_var_t, field_type_e> get_field_value_by_name(const StructType& sinst, const std::string& name,
                                                              const std::array<FieldInfo, FieldInfoN>& fields) {
  std::string_view sv{name};

  // clang-format off
  using extractor_t = field_var_t (*)(const char* base, std::size_t off);
  auto extract_i8 = [](const char* base, std::size_t off) -> field_var_t { return *reinterpret_cast<const int8_t*>(base + off); };
  auto extract_ui8 = [](const char* base, std::size_t off) -> field_var_t { return *reinterpret_cast<const uint8_t*>(base + off); };
  auto extract_i16 = [](const char* base, std::size_t off) -> field_var_t { return *reinterpret_cast<const int16_t*>(base + off); };
  auto extract_ui16 = [](const char* base, std::size_t off) -> field_var_t { return *reinterpret_cast<const uint16_t*>(base + off); };
  auto extract_i32 = [](const char* base, std::size_t off) -> field_var_t { return *reinterpret_cast<const int32_t*>(base + off); };
  auto extract_i64 = [](const char* base, std::size_t off) -> field_var_t { return *reinterpret_cast<const int64_t*>(base + off); };
  auto extract_ui64 = [](const char* base, std::size_t off) -> field_var_t { return *reinterpret_cast<const uint64_t*>(base + off); };
  auto extract_ui32 = [](const char* base, std::size_t off) -> field_var_t { return *reinterpret_cast<const uint32_t*>(base + off); };
  auto extract_f32 = [](const char* base, std::size_t off) -> field_var_t { return *reinterpret_cast<const float*>(base + off); };
  auto extract_f64 = [](const char* base, std::size_t off) -> field_var_t { return *reinterpret_cast<const double*>(base + off); };
  auto extract_cstrarr = [](const char* base, std::size_t off) -> field_var_t {
    const char* p = reinterpret_cast<const char*>(base + off);
    return std::string(p);
  };
  auto extract_cstrptr = [](const char* base, std::size_t off) -> field_var_t {
    const char* p = *reinterpret_cast<const char* const*>(base + off);
    return p ? std::string(p) : std::string();
  };

  static constexpr std::array<extractor_t, static_cast<size_t>(field_type_count)> extractor_table = {
      extractor_t(+extract_i8),  extractor_t(+extract_ui8), extractor_t(+extract_i16),     extractor_t(+extract_ui16),
      extractor_t(+extract_i32), extractor_t(+extract_i64), extractor_t(+extract_ui64),    extractor_t(+extract_ui32),
      extractor_t(+extract_f32), extractor_t(+extract_f64), extractor_t(+extract_cstrarr), extractor_t(+extract_cstrptr)};
  

  auto pred = [&sv](const FieldInfo& field) { return field.name == sv; };
  const auto it = std::find_if(fields.begin(), fields.end(), pred);
  if (it != fields.end()) {
    const char* base = reinterpret_cast<const char*>(&sinst);
    const std::size_t off = it->offset;

    const auto idx = static_cast<size_t>(it->type);
    if (idx < extractor_table.size()) {
      const extractor_t ext = extractor_table[idx];
      return {ext(base, off), it->type};
    }
  }
  // clang-format on

  return {field_var_t{}, field_type_e{}};
}

实现关键点:根据定义的可解析数据类型,定义对应的提取函数 extractor_t,并建立提取函数表 extractor_table。根据字段名称查找字段信息,然后根据字段类型,调用对应的提取函数,获取字段值。最后,封装为 field_var_t 返回。 字段类型信息,来源于手动定义的字段描述信息数组 fields。根据字段名称查找得到该字段的 field_type_e。

3.3. 另外一个版本:根据字段名称(field name)获取结构体中该字段的值,直接返回类型T

1
2
3
4
5
6
7
8
9
10
template <typename T>
std::optional<T> make_copy(const void* src, std::size_t sz) {
  static_assert(std::is_trivially_copyable_v<T>, "T must be trivially copyable");
  if (sizeof(T) != sz)
    return std::nullopt;

  T v;
  std::memcpy(&v, src, sz);
  return v;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
template <typename StructType, typename FieldType, size_t FieldInfoN>
std::optional<FieldType> get_field_value_by_name_as(const StructType& sinst, const std::string& name,
                                                    const std::array<FieldInfo, FieldInfoN>& fields) {
  std::string_view sv{name};
  auto it = std::find_if(fields.begin(), fields.end(), [&](const FieldInfo& f) {
    return f.name == sv;
  });
  if (it == fields.end())
    return std::nullopt;

  const char* base = reinterpret_cast<const char*>(&sinst);
  const std::size_t off = it->offset;
  const field_type_e ft = it->type;

  if constexpr (std::is_trivially_copyable_v<FieldType>) {
    using extractor_t = std::optional<FieldType> (*)(const char* base, std::size_t off);

    // clang-format off
    // lambdas must be non-capturing to convert to function pointer
    auto ex_i8 = [](const char* base, std::size_t off) -> std::optional<FieldType> { return make_copy<FieldType>(base + off, sizeof(int8_t)); };
    auto ex_ui8 = [](const char* base, std::size_t off) -> std::optional<FieldType> { return make_copy<FieldType>(base + off, sizeof(uint8_t)); };
    auto ex_i16 = [](const char* base, std::size_t off) -> std::optional<FieldType> { return make_copy<FieldType>(base + off, sizeof(int16_t)); };
    auto ex_ui16 = [](const char* base, std::size_t off) -> std::optional<FieldType> { return make_copy<FieldType>(base + off, sizeof(uint16_t)); };
    auto ex_i32 = [](const char* base, std::size_t off) -> std::optional<FieldType> { return make_copy<FieldType>(base + off, sizeof(int32_t)); };
    auto ex_ui32 = [](const char* base, std::size_t off) -> std::optional<FieldType> { return make_copy<FieldType>(base + off, sizeof(uint32_t)); };
    auto ex_i64 = [](const char* base, std::size_t off) -> std::optional<FieldType> { return make_copy<FieldType>(base + off, sizeof(int64_t)); };
    auto ex_ui64 = [](const char* base, std::size_t off) -> std::optional<FieldType> { return make_copy<FieldType>(base + off, sizeof(uint64_t)); };
    auto ex_f32 = [](const char* base, std::size_t off) -> std::optional<FieldType> { return make_copy<FieldType>(base + off, sizeof(float)); };
    auto ex_f64 = [](const char* base, std::size_t off) -> std::optional<FieldType> { return make_copy<FieldType>(base + off, sizeof(double)); };
    auto ex_invalid = [](const char* /*base*/, std::size_t /*off*/) -> std::optional<FieldType> { return std::nullopt; };

    static const std::array<extractor_t, static_cast<size_t>(field_type_count)> extractors = {
        extractor_t(+ex_i8),  extractor_t(+ex_ui8), extractor_t(+ex_i16),     extractor_t(+ex_ui16),
        extractor_t(+ex_i32), extractor_t(+ex_i64), extractor_t(+ex_ui64),    extractor_t(+ex_ui32),
        extractor_t(+ex_f32), extractor_t(+ex_f64), extractor_t(+ex_invalid), extractor_t(+ex_invalid)};
    // clang-format on

    const auto idx = static_cast<size_t>(ft);
    if (idx < extractors.size()) {
      return extractors[idx](base, off);
    }
    return std::nullopt;
  }

  // 非 trivially copyable
  if constexpr (std::is_same_v<FieldType, std::string>) {
    if (ft == field_type_cstrarr) {
      const char* p = reinterpret_cast<const char*>(base + off);
      return std::string(p);
    }
    if (ft == field_type_cstrptr) {
      const char* const* pp = reinterpret_cast<const char* const*>(base + off);
      return pp && *pp ? std::string(*pp) : std::string{};
    }
    return std::nullopt;
  }

  return std::nullopt;
}

实现关键点:同样根据定义的可解析数据类型,定义对应的提取函数 extractor_t,并建立提取函数表 extractors。根据字段名称查找字段信息,然后根据字段类型,调用对应的提取函数,获取字段值。最后,封装为 std::optional 返回。 字段类型信息的获取,与函数 get_field_value_by_name 相同。

3.4. 根据字段名称(field name),设置结构体中该字段的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
template <typename StructType, typename FieldType, size_t FieldInfoN>
inline ptrdiff_t set_field_by_name(StructType& ui, const std::string& name, const FieldType& value,
                                   const std::array<FieldInfo, FieldInfoN>& fields, size_t cstrarr_size = 0) {
  std::string_view sv{name};
  auto it = std::find_if(fields.begin(), fields.end(), [&](const FieldInfo& f) {
    return f.name == sv;
  });
  if (it == fields.end())
    return -1;

  char* base = reinterpret_cast<char*>(&ui);
  const auto offset = static_cast<ptrdiff_t>(it->offset);

  if constexpr (std::is_trivially_copyable_v<FieldType>) {
    if (field_type_sizes[it->type] == sizeof(FieldType)) {
      std::memcpy(base + offset, &value, sizeof(FieldType));
      return offset;
    }
    return -2;  // 类型不匹配
  }

  if constexpr (std::is_convertible_v<FieldType, const char*>) {  // char*, char[], or string literal
    if (it->type == field_type_cstrarr) {
      if (cstrarr_size == 0)
        return -3;  // 需指定数组长度
      char* arr = base + offset;
      std::strncpy(arr, static_cast<const char*>(value), cstrarr_size - 1);
      arr[cstrarr_size - 1] = '\0';
      return offset;
    }
    if (it->type == field_type_cstrptr) {
      char** pptr = reinterpret_cast<char**>(base + offset);
      if (*pptr) {
        std::strcpy(*pptr, static_cast<const char*>(value));
        return offset;
      }
      return -4;  // 指针为空
    }
    return -2;
  }

  // 支持 std::string
  if constexpr (std::is_same_v<FieldType, std::string>) {
    if (it->type == field_type_cstrarr) {
      if (cstrarr_size == 0)
        return -3;
      char* arr = base + offset;
      std::strncpy(arr, value.c_str(), cstrarr_size - 1);
      arr[cstrarr_size - 1] = '\0';
      return offset;
    }
    if (it->type == field_type_cstrptr) {
      char** pptr = reinterpret_cast<char**>(base + offset);
      if (*pptr) {
        std::strcpy(*pptr, value.c_str());
        return offset;
      }
      return -4;
    }
    return -2;
  }

  return -5;  // 不支持的类型
}

3.5. 调用实例

定义映射表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static_assert(std::is_standard_layout_v<ps_formation_param_t>, "userinfo not standard layout");

constexpr size_t kFormationParamNamesCount =
    boost::pfr::tuple_size<ps_formation_param_t>::value + ps_formation_param_t::kVTableSize - 1;

using namespace struct_reflect;

// clang-format off
constexpr std::array<struct_reflect::FieldInfo, kFormationParamNamesCount> kFormationParamNames = {
    FieldInfo{std::string_view("Formation_Desired_Position_Offset_N"),offsetof(ps_formation_param_t, Formation_Desired_Position_Offset_N),field_type_e::field_type_f64},
    FieldInfo{std::string_view("Formation_Desired_Position_Offset_E"),offsetof(ps_formation_param_t, Formation_Desired_Position_Offset_E),field_type_e::field_type_f64},
    FieldInfo{std::string_view("Formation_Desired_Position_Offset_D"),offsetof(ps_formation_param_t, Formation_Desired_Position_Offset_D),field_type_e::field_type_f64},
    FieldInfo{std::string_view("swarmNum"),offsetof(ps_formation_param_t, swarmNum),field_type_e::field_type_ui8},
    FieldInfo{std::string_view("hController_k"),offsetof(ps_formation_param_t, hController_k),field_type_e::field_type_f64},
    FieldInfo{std::string_view("hController_f"),offsetof(ps_formation_param_t, hController_f),field_type_e::field_type_f64},
    FieldInfo{std::string_view("hController_D"),offsetof(ps_formation_param_t, hController_D),field_type_e::field_type_f64},
    FieldInfo{std::string_view("coefs_k"),offsetof(ps_formation_param_t, coefs_k),field_type_e::field_type_f64},
    FieldInfo{std::string_view("coefs_f"),offsetof(ps_formation_param_t, coefs_f),field_type_e::field_type_f64},
    FieldInfo{std::string_view("coefs_u2a"),offsetof(ps_formation_param_t, coefs_u2a),field_type_e::field_type_f64},
    FieldInfo{std::string_view("coefs_uff"),offsetof(ps_formation_param_t, coefs_uff),field_type_e::field_type_f64},
    FieldInfo{std::string_view("vTable_0"),offsetof(ps_formation_param_t, vTable) + sizeof(double) * 0,field_type_e::field_type_f64},
    FieldInfo{std::string_view("vTable_1"),offsetof(ps_formation_param_t, vTable) + sizeof(double) * 1,field_type_e::field_type_f64},
    FieldInfo{std::string_view("vTable_2"),offsetof(ps_formation_param_t, vTable) + sizeof(double) * 2,field_type_e::field_type_f64},
    FieldInfo{std::string_view("vTable_3"),offsetof(ps_formation_param_t, vTable) + sizeof(double) * 3,field_type_e::field_type_f64},
    FieldInfo{std::string_view("vTable_4"),offsetof(ps_formation_param_t, vTable) + sizeof(double) * 4,field_type_e::field_type_f64},
    FieldInfo{std::string_view("vTable_5"),offsetof(ps_formation_param_t, vTable) + sizeof(double) * 5,field_type_e::field_type_f64},
    FieldInfo{std::string_view("vTable_6"),offsetof(ps_formation_param_t, vTable) + sizeof(double) * 6,field_type_e::field_type_f64},
    FieldInfo{std::string_view("vTable_7"),offsetof(ps_formation_param_t, vTable) + sizeof(double) * 7,field_type_e::field_type_f64},
    FieldInfo{std::string_view("hSafe"),offsetof(ps_formation_param_t, hSafe),field_type_e::field_type_f64},
    FieldInfo{std::string_view("asb_offset_n"),offsetof(ps_formation_param_t, asb_offset_n),field_type_e::field_type_f64},
    FieldInfo{std::string_view("asb_offset_e"),offsetof(ps_formation_param_t, asb_offset_e),field_type_e::field_type_f64},
    FieldInfo{std::string_view("L1"),offsetof(ps_formation_param_t, L1),field_type_e::field_type_f64},
    FieldInfo{std::string_view("kVel"),offsetof(ps_formation_param_t, kVel),field_type_e::field_type_f64},
    FieldInfo{std::string_view("robustLat"),offsetof(ps_formation_param_t, robustLat),field_type_e::field_type_f64},
    FieldInfo{std::string_view("asbController_kPos"),offsetof(ps_formation_param_t, asbController_kPos),field_type_e::field_type_f64},
    FieldInfo{std::string_view("asbController_kAsb"),offsetof(ps_formation_param_t, asbController_kAsb),field_type_e::field_type_f64},
    FieldInfo{std::string_view("asbController_tol1"),offsetof(ps_formation_param_t, asbController_tol1),field_type_e::field_type_f64},
    FieldInfo{std::string_view("asbController_tol2"),offsetof(ps_formation_param_t, asbController_tol2),field_type_e::field_type_f64},
    FieldInfo{std::string_view("asbController_r1"),offsetof(ps_formation_param_t, asbController_r1),field_type_e::field_type_f64},
    FieldInfo{std::string_view("asbController_r2"),offsetof(ps_formation_param_t, asbController_r2),field_type_e::field_type_f64},
    FieldInfo{std::string_view("fmtController_kLat"),offsetof(ps_formation_param_t, fmtController_kLat),field_type_e::field_type_f64},
    FieldInfo{std::string_view("fmtController_kProj"),offsetof(ps_formation_param_t, fmtController_kProj),field_type_e::field_type_f64},
    FieldInfo{std::string_view("fmtWpStartIdx"),offsetof(ps_formation_param_t, fmtWpStartIdx),field_type_e::field_type_ui16},
    FieldInfo{std::string_view("takeIdx"),offsetof(ps_formation_param_t, takeIdx),field_type_e::field_type_ui16},
    FieldInfo{std::string_view("returnIdx"),offsetof(ps_formation_param_t, returnIdx),field_type_e::field_type_ui16},
    FieldInfo{std::string_view("Vn"),offsetof(ps_formation_param_t, Vn),field_type_e::field_type_f64},
    FieldInfo{std::string_view("rm"),offsetof(ps_formation_param_t, rm),field_type_e::field_type_f64},
};
// clang-format on

在此,对结构体作限制:要求结构体以及其字段均为标准布局类型(standard layout type),以确保字段偏移地址的正确性。

其他代码省略。

本文由作者按照 CC BY 4.0 进行授权