最近在工作中遇到了需要将 C++ 结构体序列化为 CSV 的需求,而且需要将不同类型的结构体序列化到同一行,一开始我手动数了不同结构的字段数并为每个结构体写了一个序列化函数,用空字符串填充属于其他类型的列,很快我意识到这十分愚蠢,一旦发生变化要改的地方很多,还很容易改错。于是我想能不能利用现代 C++ 的特性,让编译器自动帮我做这些事。
Use boost::pfr to implement a generic CSV serialization library, flatten nested struct with field name as prefix, and recursively expand std::optional and std::array
// Trait to check if a type is std::optional template<typename T> structis_optional : std::false_type {}; template<typename T> structis_optional<std::optional<T>> : std::true_type {};
// Trait to check if a type is reflectable by Boost.PFR (i.e., an aggregate struct) template<typename T, typename = void> struct is_boost_pfr_reflectable : std::false_type {};
template<typename T> structis_boost_pfr_reflectable<T, std::void_t<decltype(boost::pfr::detail::fields_count<T>())>> : std::is_aggregate<T> {}; // Boost.PFR primarily works with aggregate types.
template<typename T> voidgenerate_header_recursive(std::vector<std::string> &headers, const std::string &prefix){ // If T is std::optional<U>, generate headers for U with the same prefix. ifconstexpr(is_optional<T>::value){ using U = typename T::value_type; generate_header_recursive<U>(headers, prefix); } elseifconstexpr (std_char_array_type<T>) { headers.emplace_back(prefix.empty() ? "value" : prefix); } elseifconstexpr (std_array_type<T>) { using ElemType = typename T::value_type; constexpr std::size_t ArraySize = std::tuple_size_v<T>; ifconstexpr(ArraySize == 0)return; for (std::size_t i = 0; i < ArraySize; ++i) { std::string new_prefix = (prefix.empty() ? "" : prefix + "_") + std::to_string(i); generate_header_recursive<ElemType>(headers, new_prefix); } } // If T is a PFR-reflectable struct, iterate its fields. elseifconstexpr (is_boost_pfr_reflectable<T>::value) { // If the struct is empty (has no fields), it contributes no headers. ifconstexpr (boost::pfr::detail::fields_count<T>() == 0) { return; } // Iterate over fields of T. boost::pfr::for_each_field_with_name(T{}, [&](std::string_view name, constauto &v) { using FieldType = std::decay_t<decltype(v)>; // Get the name of the current field. std::string field_name_str = std::string(name); // Construct the new prefix for nested fields. std::string new_prefix = prefix.empty() ? field_name_str : prefix + "_" + field_name_str; generate_header_recursive<FieldType>(headers, new_prefix); }); } // Base case: T is a fundamental type. The current prefix is its full header name. else { headers.emplace_back( prefix.empty() ? "value" : prefix); // Use "value" if prefix is empty (e.g. top-level basic type) } }
这里使用了 if constexpr 来进行编译期分支选择,没有被选中的分支不会报错。此外,boost::pfr::for_each_field_with_name 需要 C++ 20。
template<typename T> constevalsize_tcount_fields_recursive(){ using DecayedT = std::remove_cv_t<T>; // If T is std::optional<U>, count fields for U. ifconstexpr(is_optional<DecayedT>::value){ using U = typename DecayedT::value_type; // The type contained in std::optional returncount_fields_recursive<U>(); } elseifconstexpr (std_char_array_type<DecayedT>) { return1; } elseifconstexpr (std_array_type<DecayedT>) { using ElemType = typename DecayedT::value_type; constexpr std::size_t ArraySize = std::tuple_size_v<DecayedT>; ifconstexpr(ArraySize == 0)return 0; size_t count = 0; for (std::size_t i = 0; i < ArraySize; ++i) { count += count_fields_recursive<ElemType>(); } return count; } // If T is a PFR-reflectable struct, sum counts of its fields. elseifconstexpr (is_boost_pfr_reflectable<DecayedT>::value) { size_t count = 0; // Iterate over fields of T (using a default-constructed T for type deduction). boost::pfr::for_each_field(DecayedT{}, [&](constauto &v /*field_prototype*/, size_t) { // Deduce the type of the current field. using FieldType = std::decay_t<decltype(v)>; count += count_fields_recursive<FieldType>(); }); return count; } // Base case: T is a fundamental type or non-PFR struct, counts as 1 field. else { return1; } }
对于基本类型(如 int , double 等)和 std::array<char,N> ,返回 1。
template<typename T> voidserialize_row_recursive(const T &value, std::vector<std::string> &row_values){ using ActualT = std::remove_cv_t<std::remove_reference_t<T>>; // If T is std::optional<U> ifconstexpr(is_optional<ActualT>::value){ if (value) { // If optional has a value serialize_row_recursive(*value, row_values); } else { // If optional is empty (std::nullopt) using U = typename ActualT::value_type; size_t num_empty_fields = count_fields_recursive<U>(); for (size_t i = 0; i < num_empty_fields; ++i) { row_values.emplace_back(""); // Add empty strings for missing fields } } } elseifconstexpr (std_char_array_type<ActualT>) { row_values.emplace_back(std::string(value.data())); } elseifconstexpr (std_array_type<ActualT>) { constexpr std::size_t ArraySize = std::tuple_size_v<ActualT>; ifconstexpr(ArraySize == 0)return; for (std::size_t i = 0; i < ArraySize; ++i) { serialize_row_recursive(value[i], row_values); } } // If T is a PFR-reflectable struct elseifconstexpr (is_boost_pfr_reflectable<T>::value) { // If the struct is empty, it contributes no values. ifconstexpr (boost::pfr::detail::fields_count<T>() == 0) { return; } // Iterate over fields of the struct instance `value`. boost::pfr::for_each_field(value, [&](constauto &field_val, size_t/*field_idx*/) { serialize_row_recursive(field_val, row_values); }); } // Base case: T is a fundamental type. Convert to string. else { std::ostringstream oss; ifconstexpr(std::is_same_v<std::decay_t<T>, bool>){ oss << (value ? "true" : "false"); } else { // This handles integers, floats, std::string, etc. // std::string will be output as-is by oss, escaping happens later. oss << value; } row_values.push_back(oss.str()); } }
对于基本类型(如 int , double 等)和 std::array<char,N> ,转换为字符串输出。
template<typename Container> voidserialize_to_csv_stream(std::ostream &out, const Container &data){ // Deduce the type of objects in the container. using T = typename Container::value_type;
// Print the header row. out << generate_csv_header<T>() << "\n";
// Print each data row. for (constauto &item: data) { out << serialize_to_csv_row(item) << "\n"; } }
Limitations
本文所述实现存在一定限制:
不能处理 C 数组。存在 C 数组的结构体会在 Boost.PFR 编译时报错。但定长 C 数组可以使用 std::array 代替。
待序列化的类型 T 必须是默认可构造的,即必须存在无参数构造函数。
Boost.PFR 能处理的类型必须是 SimpleAggregate ,即不能存在基类、const 字段、引用和 C 数组。