You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++17下优化基于std::index_sequence的反序列化代码方案咨询

问题描述

我实现了一套序列化函数,可对满足以下条件的任意结构体/类计算大小、写入内存缓冲区以及从缓冲区读取:结构体/类需包含特定成员类型和as_tuple函数。

实现思路是通过返回绑定所有成员变量引用的std::tie,确保元组的第一个元素是标识具体类型的enum class变量,随后用std::apply处理各个序列化函数。

在读取缓冲区时,需要对元组的第一个成员(结构体类型变量)进行特殊处理,目前我用了std::index_sequence技巧来获取元组中每个变量的位置,但感觉代码繁琐冗余。

限定使用C++17,请勿提及std::memcpy序列化的字节序等问题。

请问在使用std::apply时,有没有更简便的方法检测元组的第一个成员并进行特殊处理?


原始代码
#include <tuple>
#include <string>
#include <vector>
#include <iostream>
#include <ostream>
#include <cstring>
#include <utility>

enum class TYPES { TYPE_A, TYPE_B, TYPE_C, TYPE_D };

struct Structure
{
    int a1;
    int a2;
    int a3;
    bool b1;
    double c1;
    double c2;
    double c3;
    double c4;
    std::string d1;
    std::string d2;

    constexpr static inline auto type{ TYPES::TYPE_A };
    constexpr auto as_tuple() { return std::tie(type, a1, a2, a3, b1, c1, c2, c3, c4, d1, d2); }
};

// SIZE
template<class T>
constexpr size_t size_of(T&& val) { 
    using Type = std::decay_t<T>;
    if constexpr (!std::is_same_v<Type, std::string>) return sizeof(T);
    else                                              return sizeof(char) * val.size() + sizeof(char);
}

template<class... Ts>
constexpr size_t size(std::tuple<Ts...>&& tuple) {
    return std::apply([](auto&&... args) { return (size_of(args) + ...); }, tuple);
}

template<class T>
constexpr size_t size(T&& object) {
    return size(std::forward<T>(object).as_tuple());
}

// WRITE
template<class T>
constexpr void write_data(unsigned char* &data, T&& val) {
    using Type = std::decay_t<T>;
    if constexpr (!std::is_same_v<Type, std::string>) { std::memcpy(data, &val, sizeof(T)); data += sizeof(T); }
    else                                              { std::memcpy(data, val.c_str(), sizeof(char) * val.size()); data += sizeof(char) * val.size(); *data++ = 0; }
}

template<class... Ts>
constexpr void write(unsigned char* data, std::tuple<Ts...>&& tuple) {
    std::apply([&](auto&&... args) { (write_data(data, args), ...); }, tuple);
}

template<class T>
constexpr void write(unsigned char* data, T&& object) {
    write(data, std::forward<T>(object).as_tuple());
}

// READ -> std::index_sequence used, is there any other way to achieve this?
template<std::size_t I, class T>
constexpr void read_data(const unsigned char* &data, T&& val) {
    using Type = std::decay_t<T>;
    if      constexpr (I == 0)                             { static_assert(std::is_same_v<Type, TYPES>); data += sizeof(Type); }
    else if constexpr (!std::is_same_v<Type, std::string>) { std::memcpy(&val, data, sizeof(Type));      data += sizeof(Type); }
    else                                                   { val = std::string{ reinterpret_cast<const char*>(data) }; data += sizeof(char) * val.size() + sizeof(char); }
}

template<std::size_t... I, class... Ts>
constexpr void read(const unsigned char* data, std::tuple<Ts...>&& tuple, std::index_sequence<I...>) {
    static_assert(sizeof...(I) == sizeof...(Ts));
    std::apply([&](auto&&... args) { (read_data<I>(data, args), ...); }, tuple);
}

template<class T>
constexpr void read(const unsigned char* data, T&& object) {
    constexpr auto size = std::tuple_size_v<decltype(std::forward<T>(object).as_tuple())>;
    read(data, std::forward<T>(object).as_tuple(), std::make_index_sequence<size>{});
}

template<class... Ts>
void print_tuple(std::tuple<Ts...>&& tuple)
{
    std::apply([&](auto&&... args) { bool once{}; ((std::cout << (std::exchange(once, true) ? ", " : "") << args), ...) << "\n"; }, tuple);
}

template<class T>
void print_tuple(T&& object)
{
    print_tuple(std::forward<T>(object).as_tuple());
}

std::ostream& operator<< (const std::ostream& out, TYPES type)
{
    return std::cout << "Type_" << static_cast<int>(type);
}

int main()
{
    Structure original{ 1, 2, 3, true, 1.1, 2.2, 3.3, 4.4, "hello", "goodbye" };
    auto length = size(original);
    std::vector<unsigned char> buffer(length);
    write(buffer.data(), original);

    Structure copy;
    read(buffer.data(), copy);

    print_tuple(original);
    print_tuple(copy);

    return 0;
}

解决方案

可以直接拆分元组的第一个元素和剩余元素,完全避免使用std::index_sequence,代码逻辑会更简洁直观。核心思路是单独处理第一个类型标识元素,剩余元素通过std::apply批量处理。

修改后的read相关函数如下:

// 处理非第一个元素的通用读取逻辑
template<class T>
constexpr void read_non_first(const unsigned char* &data, T&& val) {
    using Type = std::decay_t<T>;
    if constexpr (!std::is_same_v<Type, std::string>) {
        std::memcpy(&val, data, sizeof(Type));
        data += sizeof(Type);
    } else {
        val = std::string{ reinterpret_cast<const char*>(data) };
        data += sizeof(char) * val.size() + sizeof(char);
    }
}

// 处理元组的读取:单独处理第一个元素,剩余元素批量处理
template<class First, class... Rest>
constexpr void read_tuple(const unsigned char* &data, std::tuple<First, Rest...>&& tuple) {
    // 处理第一个类型标识元素:直接跳过缓冲区数据(目标对象的type是静态常量)
    static_assert(std::is_same_v<std::decay_t<First>, TYPES>);
    data += sizeof(First);

    // 提取剩余元素并批量处理
    std::apply([&](auto&&... args) { (read_non_first(data, args), ...); }, 
               std::make_tuple(std::get<Rest>(tuple)...));
}

// 对外接口保持不变
template<class T>
constexpr void read(const unsigned char* data, T&& object) {
    read_tuple(data, std::forward<T>(object).as_tuple());
}

说明

  1. 单独提取元组的第一个元素做特殊处理:因为目标对象的type是静态常量,不需要从缓冲区加载,直接跳过对应长度的字节即可。
  2. 剩余元素通过std::make_tuple(std::get<Rest>(tuple)...)提取,再用std::apply批量调用通用读取函数,完全不需要索引序列。
  3. 代码结构更清晰,去掉了冗余的模板索引逻辑,符合C++17的特性。

内容的提问来源于stack exchange,提问作者Pablo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 09:06:00