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()); }
说明
- 单独提取元组的第一个元素做特殊处理:因为目标对象的
type是静态常量,不需要从缓冲区加载,直接跳过对应长度的字节即可。 - 剩余元素通过
std::make_tuple(std::get<Rest>(tuple)...)提取,再用std::apply批量调用通用读取函数,完全不需要索引序列。 - 代码结构更清晰,去掉了冗余的模板索引逻辑,符合C++17的特性。
内容的提问来源于stack exchange,提问作者Pablo
相关产品推荐
相关产品推荐

