元编程在编译期间检查struct元素的内容一致性(C++)

时间:2024-10-21 22:04:24

文章目录

  • 前言
  • 一、概述
  • 二、模板元编程的组成要素
  • 三、C++ 结构体字段反射
  • 四、静态反射
  • 五、人工手写序列化/反序列化代码、动态反射
  • 六、std::hash


前言

C++模板元编程能够实现编译期运算的根本是利用了编译器会对模板参数进行推导(类型,变量)。


一、概述

元编程(Metaprogramming)是指某类计算机程序的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。(百度上的定义)

简单来说就是能在编译器处理一些程序,或进行一些运算就算元编程。

二、模板元编程的组成要素

模板元编程产生的源程序是在编译期执行的程序,因此它首先要遵循C++和模板的语法,但是它操作的对象不是运行时普通的变量,因此不能使用运行时的C++关键字(如if、else、for),可用的语法元素相当有限,最常用的是:

enumstatic const //用来定义编译期的整数常量;
typedef/using  //用于定义元数据;[类型别名]
T/Args...  //声明元数据类型;【模版参数:类型形参,非类型形参】
Template //主要用于定义元函数;【模版类,特化,偏特化】
::  //域运算符,用于解析类型作用域获取计算结果(元数据)。【获取元数据,元类型】
  • 1
  • 2
  • 3
  • 4
  • 5

constexpr关键字:constexpr关键字,这个关键字修饰的常量是编译期常量,也就是说,在编译期中这个常量的值就可以被确定。例子:constexpr int MAX = 100;
在这里插入图片描述

三、C++ 结构体字段反射

这里探究一下结构体 (struct) 的 字段 (field) 反射,如结构体有哪些字段,每个字段在结构体中的位置是什么?

给定两个 C++ 结构体 SimpleStruct 和 NestedStruct:

struct SimpleStruct {
  bool bool_;
  int int_;
  double double_;
  std::string string_;
  std::unique_ptr<bool> optional_;
};

struct NestedStruct {
  SimpleStruct nested_;
  std::vector<SimpleStruct> vector_;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

NestedStruct::nested_ 为嵌套对象,NestedStruct::vector_ 为嵌套的对象数组;

实现从 C++ 结构体到 JSON 的序列化/反序列化操作,需要用到以下信息:

结构体有 哪些字段

bool_/int_/double_/string_/optional_
nested_/vector_
  • 1
  • 2

每个 字段 在 结构体中 的什么 位置

&SimpleStruct::bool_/&SimpleStruct::int_/&SimpleStruct::double_/&SimpleStruct::string_/&SimpleStruct::optional_
&NestedStruct::nested_/&NestedStruct::vector_
  • 1
  • 2

每个 字段 在 JSON 中 对应的 名称 是什么

"_bool"/"_int"/"_double"/"_string"/"_optional"
"_nested"/"_vector"
  • 1
  • 2

四、静态反射

实际上,实现序列化/反序列化所需要的信息(有哪些字段,每个字段的位置、名称、映射方法),在编译时 (compile-time) 就已经确定了,所以就没必要在运行时 (runtime) 动态构建对象。所以,我们可以利用 静态反射 (static reflection) 的方法,把这些信息告诉编译器,让它帮我们生成代码。

利用访问者模式 (visitor pattern),使用元组 std::tuple 记录结构体所有的字段信息,通过编译时多态 (compile-time polymorphism) 针对具体的 字段类型 进行转换操作。

首先,定义一个 StructSchema 函数模板 (function template),返回所有字段信息(默认返回空元组):

template <typename T>
inline constexpr auto StructSchema() {
  return std::make_tuple();
}
  • 1
  • 2
  • 3
  • 4

提供 DEFINE_STRUCT_SCHEMA 和 DEFINE_STRUCT_FIELD 两个宏 (macro) ,定义结构体字段信息(有哪些、位置、名称):

#define DEFINE_STRUCT_SCHEMA(Struct, ...)        \
  template <>                                    \
  inline constexpr auto StructSchema<Struct>() { \
    using _Struct = Struct;                      \
    return std::make_tuple(__VA_ARGS__);         \
  }

#define DEFINE_STRUCT_FIELD(StructField, FieldName) \
  std::make_tuple(&_Struct::StructField, FieldName)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

StructSchema 返回元组的结构是:((&field1, name1), (&field2, name2), …)
DEFINE_STRUCT_SCHEMA 定义了 结构体Struct有哪些字段
DEFINE_STRUCT_FIELD 定义了每个 字段 的 位置、名称
using _Struct = Struct 提供了一种宏内数据接力的方法,让下一个宏能获取上一个宏的数据

最后,提供 ForEachField 函数,从对应的 StructSchema 取出记录结构体 StructType 所有字段信息的元组,然后遍历这个元组,从中取出每个字段的位置、名称,作为参数调用转换函数fn:

template <typename T, typename Fn>
inline constexpr void ForEachField(T&& value, Fn&& fn) {
  constexpr auto struct_schema = StructSchema<std::decay_t<T>>();
  detail::ForEachTuple(struct_schema, [&value, &fn](auto&& field_schema) {
    fn(value.*(std::get<0>(std::forward<decltype(field_schema)>(field_schema))),
       std::get<1>(std::forward<decltype(field_schema)>(field_schema)));
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

fn 接受的参数分别为:字段的值和名称 (field_value,field_name)

  • 字段的值通过 value.*field_pointer 得到,其中 field_pointer 是成员指针

ForEachTuple 的实现中还用到了静态断言(static assert) 检查,具体见代码:/articles/2018/Cpp-Struct-Field-Reflection/static_reflection.h

  • 检查 StructSchema是否定义了字段信息
  • 检查每个字段的信息 是否都包含了位置和名称

总的来说分为两个步骤:

1、使用 DEFINE_STRUCT_SCHEMA 和 DEFINE_STRUCT_FIELD 静态定义字段信息(名称、位置)
2、调用 ForEachField 并传入 映射方法(泛型 functor 或泛型 lambda 表达式),对所有字段调用这个函数

// define schema (partial)
DEFINE_STRUCT_SCHEMA(
    SimpleStruct,
    DEFINE_STRUCT_FIELD(int_, "int"),
    DEFINE_STRUCT_FIELD(string_, "string"));

// use ForEachTuple
ForEachField(SimpleStruct{1, "hello static reflection"},
             [](auto&& field, auto&& name) {
               std::cout << name << ": "
                         << field << std::endl;
             });

// output:
//   int: 1
//   string: hello static reflection

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

静态反射过程中,最核心 的地方:传入 ForEachField 的可调用对象 fn,通过 编译时多态 针对不同 字段类型 选择不同的转换操作:

  • 针对 int 类型字段,ForEachField 调用 fn(simple.int_, “int”)
  • 针对 std::string 类型字段,ForEachField 调用 fn(simple.string_, “string”

静态反射的好处:1、可以通过声明式 的方法,静态定义字段信息。2、在调用 ForEachField 时,映射方法作为参数传入;利用 编译时多态的机制,为不同的 字段类型 选择合适的操作

五、人工手写序列化/反序列化代码、动态反射

/p/88144082

六、std::hash

哈希模板定义一个函数对象,实现了散列函数,这个函数对象的实例定义一个operator();

std::hash实现步骤:

1。接受一个参数的类型Key. 如元组

2。返回一个类型为size_t的值,表示该参数的哈希值.

3。调用时不会抛出异常.

4。若两个参数k1k2相等,则std::hash()(k1)== std::hash()(k2).

5。若两个不同的参数k1k2不相等,则std::hash()(k1)== std::hash()(k2)成立的概率应非常小,接近1.0/std::numeric_limits<size_t>::max().

std::hash - C++中文 - API参考文档 :/cpp-zh/cpp/utility/

以string为例,模板的专门化std::hash对于各种字符串类,允许用户获取字符串的散列。:

#include <iostream>

#include <string>

#include <functional>

int main()

{

std::string s = "Stand back! I've got jimmies!";

std::hash<std::string> hash_fn;

size_t hash = hash_fn(s);

std::cout << hash << '\n';

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

基本类型的标准特化

template<> struct hash<bool>;
template<> struct hash<char>;
template<> struct hash<signed char>;
template<> struct hash<unsigned char>;
template<> struct hash<char8_t>;        // C++20
template<> struct hash<char16_t>;
template<> struct hash<char32_t>;
template<> struct hash<wchar_t>;
template<> struct hash<short>;
template<> struct hash<unsigned short>;
template<> struct hash<int>;
template<> struct hash<unsigned int>;
template<> struct hash<long>;
template<> struct hash<long long>;
template<> struct hash<unsigned long>;
template<> struct hash<unsigned long long>;
template<> struct hash<float>;
template<> struct hash<double>;
template<> struct hash<long double>;
template<> struct hash<std::nullptr_t>;

template< class T > struct hash<T*>;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22