首页 > 编程语言 >万物容器与 c++ 类型反射

万物容器与 c++ 类型反射

时间:2024-03-02 13:44:05浏览次数:26  
标签:容器 const union void storage c++ static type 万物

这是一篇组会分享,并且是拖了很长很长时间的那种。这次不会再鸽了


这篇文章可以说是针对某 cpp 佬的公众号的两篇原创内容的笔记

c++反射--包容一切的all容器(上)

c++反射--包容一切的all容器(中)

什么是反射

这个好像没有严格的定义,但是概括的说,「反射」是指在程序运行期对程序本身进行访问修改的能力

比如以下一些例子:

  • 我们希望通过一个字符串 "func",调用名字为 "func" 的函数。

    比如 java 里可以这么写

    Class cls = Class.forName("com.reflection.A");
    Method method = cls.getMethod("introduction");
    

    这个看起来还容易用 cpp 做到类似效果

  • 写出一个函数 print(),它可以传入任意类型的结构体作为参数,然后打印其所有的成员、方法的类型

    这在 cpp 看来就像是天方夜谭了

「通过反射,我们可以在程序运行期动态获取类型信息,从而能够观察、创建以及修改一个对象」


不像 go、java 等语言提供了完善的原生反射支持,cpp 对反射属于是完全绝缘

对于静态反射:「C++无法原生支持任意对象的静态反射,基本无法做到对一个类的成员对象或方法进行遍历」。作为特例,std::tuple 提供了一些编译期工具,包括「编译期获得一个 tuple 的成员数量」,「通过一个编译期的索引值静态访问成员」。但是实际上使用效果不佳

对于动态反射:cpp 给了一个头文件 <typeinfo>,里面的组件仅提供类型的判断,对于所谓 “动态反射” 的涉及非常有限,况且组件本身也很难用...


所以这里我们尝试自己手搓一套类型系统,这种类自己想办法记录了对象的类型信息,在此之上就有机会实现一种近似反射的效果

all 容器

我们希望有一种 all 容器,满足以下性质

  • all 是一个普通类而不是一个模板类。否则不同模板的 all 不能被装进同一个 vector
  • all 能够存储各种类型。包括常规变量、引用、指针,以及他们的派生
  • 此外,我们还希望 all 能提供自动的类型转换。「例如输入的是 float 类型,使用的地方需要的是 double,我们希望容器 all 有能力把 float 转换成 double 提供给使用方。再比如容器 all 里面存储的类型 T*,而使用的地方需要的是 const T&,我们也希望容器能够提供这样的功能」

实现-引入&存储

完整的实现需要很多模板元编程的经验,并且要有对 cpp 类型系统和内存排布的理解,外加一堆细节,这可难以高攀。所以我就贴一些原文代码外加理解到的思路好了


容器 all 作为一个普通类但是需要具备构造各种类型的能力,那么类型信息从什么地方来呢?在 cpp 里操作类型也只能用模板了。我们配备一个模板构造函数来接收传入对象的类型

class all final {
public:
    template <typename T> all(T&& value);
};

然后,我们要考虑如何把这个对象的「值」和「类型」都存下来

因为 all 是一个普通类,其成员就都必须是不依赖 T 类型的普通类成员

这时候我们就要用到一种被称为「类型擦除」的技巧(非正式称呼),意思是把对象的值取出,放到一个通用的容器里,实现 “无类型” 的存下这些数据;同时把它的类型单独用模板传递

这样一来,一个对象就实现了值和类型分别单独处理


具体地说:

我们可以按照下图来存储七种类型

归结为:

  • 左值引用用指针存储
  • 右值引用直接存值
  • const TT 存储
  • 其他的保持不变

这样对于类型 T,只有三种存储类型:

  • T
  • T*
  • const T*

构造如下的通用存储类:

union storage_union {
  using stack_storage_t =
    std::aligned_storage<2 * sizeof(void*),
   	  std::alignment_of<void*>::value>::type;

  void*           dynamic;
  stack_storage_t stack;
};

我们希望如果类型 T 占的空间小于等于两个指针大小,就在栈空间上存储,否则给 void* 指针动态申请空间存储数据

判断是否使用动态分配的元函数如下:

template<typename T>
struct requires_allocation
  : std::integral_constant<bool,
!(std::is_nothrow_move_constructible<T>::value &&
  sizeof(T) <= sizeof(storage_union::stack) &&
  std::alignment_of<T>::value <=
  std::alignment_of<storage_union::stack_storage_t>::value)>
{};

all 的模板构造函数里使用

if constexpr (requires_allocation<T>::value)

即可判断。这样,就容易实现在模板构造函数中,将任意类型对象的值都合适的存储到一个通用类 storage_union 中来


接下来,我们考虑到 all 容器中需要实现一系列操作接口用来操作存储的对象

我们希望将 storage_union 作为参数传入,将模板构造函数获取的类型用模板传入,进而接口能同时得知对象的值和类型

又注意到,all 是一个普通类,其成员函数不能用模板

因此我们需要玩帽子戏法。这里的实现是首先在 all 类里面声明一系列的接口函数指针

struct vtable_type
{
  // 获取对象地址
  const void* (*address)(const storage_union&);
  // 销毁对象
  void (*destroy)(storage_union&) noexcept;
  // 复制内容
  void (*copy)(const storage_union& src, storage_union& dest);
  // 移动内容
  void (*move)(storage_union& src, storage_union& dest) noexcept;
  // swap
  void (*swap)(storage_union& lhs, storage_union& rhs) noexcept;
  // operator=(const T& rhs)对应实现
  void (*assign)(all* ptr, storage_union&);
  // operator=(T&& rhs)对应的实现
  void (*moveassign)(all* ptr, storage_union&);
};

然后考虑一个模板类 store_type,专门用来实现上文所述七种类型之一的接口,并且其成员函数都是静态函数

例如,对于类型 T&,我们有下面的模板类

template<typename T>
struct store_type<T&>
{
  using type = T*;

  static const void* address(const storage_union& storage) {
    return &storage.stack;
  }

  static void construct(T& value, storage_union& storage) {
    new (&storage.stack) T*(&value);
  }

  static void assign(all* ptr, storage_union& storage) {
    auto self = static_cast<T**>(const_cast<void*>(address(storage)));
    auto rhs  = ptr->cast_to<const T*>();
    (**self)     = *rhs;
  }

  static void moveassign(all* ptr, storage_union& storage) {
    auto self = static_cast<T*>(const_cast<void*>(address(storage)));
    auto rhs  = ptr->cast_to<T*>();
    *self     = std::move(*rhs);
  }

  static void destroy(storage_union& storage) noexcept {}

  static void copy(const storage_union& src, storage_union& dest) {
    auto src_ptr = (const T**)(&(src.stack));
    new (&dest.stack) T*(*const_cast<T**>(src_ptr));
  }

  static void move(storage_union& src, storage_union& dest) noexcept {
    new (&dest.stack) T*(*reinterpret_cast<T**>(&src.stack));
  }

  static void swap(storage_union& lhs, storage_union& rhs) noexcept {
    T* src = *reinterpret_cast<T**>(&lhs.stack);
    T* dst = *reinterpret_cast<T**>(&rhs.stack);
    auto temp = src;
    src = dst;
    dst = temp;
  }
};

然后,在 all 的模板构造函数中,令 all 容器的接口指针一一指向对应类型的模板类的静态成员函数,就可以了

「到此为止,我们就解决了类型如何『引入』以及『保持』的问题,已经可以把需要存储的三种类型存储到没有类型的空间,并且把必要的类型信息通过模版静态函数的方式进行进行抹除,这样在容器 all 里面看起来是类型无关的」


实现-类型转换 & 跨平台类型 id

我们希望最后达到类似这样的效果:

all a((short)(0));
int b = a.to<int>();

1 转换器生成

构造一个 converter 模板类,里面放一个接收 void* 参数的静态成员函数 convert,例如:

template<typename SRC, typename DST>
struct type_converter {
    static bool convert(const void* src, void* dst) noexcept {
        auto dst_ptr = reinterpret_cast<DST*>(dst);
        auto src_ptr = reinterpret_cast<const SRC*>(src);
        *dst_ptr     = static_cast<DST>(*src_ptr);

        return true;
    }
};

好处和上文的类型擦除是一样的:这样所有的转换器的类型都是 bool (*)(const void*, void*) noexcept,我们方便统筹起来管理,例如这样是可行的:

using F = bool (*)(const void*, void*) noexcept;
std::vector<F> vec;
vec.push_back(type_converter<short, int>::convert)

对于 type_converter 来说,对于以下情况进行特化:

这样以来,对于任何合法的 SRC -> DST 的转化,取 type_converter<SRC, DST>::convert 就自动生成了对应的转换器

2 转换器管理

我们增加一个管理类用来管理所有的转换器

  • 使用单例,无论注册还是获取转换函数都使用同一个实例
  • 可以添加转换函数,也可以查询转换函数

all 从管理类获取转换器,方便我们精细控制类型之间的转化规则。同时也方便自定义特定的类型转换

class type_conversion final {
public:
    static type_conversion& get_instance();
    using F = bool (*)(const void*, void*) noexcept;

    bool has_converter(uint32_t src, uint32_t dst) const noexcept;
    F find_converter(uint32_t src, uint32_t dst) const noexcept;
    void add_converter(uint32_t src, uint32_t dst, F f) noexcept;

private:
    type_conversion();
    type_conversion(const type_conversion&) = delete;
    type_conversion(type_conversion&&)      = delete;

    type_conversion& operator=(const type_conversion&) = delete;
    type_conversion& operator=(type_conversion&&)      = delete;

private:
    std::unordered_map<uint64_t, F> m_convert_list;
} conversion;

可以看到,这个管理类将源类型和目标类型都用一个 uint32_t 表示,以此来提高效率

接下来的问题显然是,「怎么由一个类型产生一个唯一 id」

跨平台类型 id

注意到 cpp 原生的组件 type_info::name 并不能产生跨平台的唯一 id,在不同编译环境下用 type_info 得到的东西可能不一样。所以我们需要自己造轮子

如果可以保证不同平台上的类型名字符串是一致的话,直接使用类型名是一个比较不错的选择。比如

int --> "int"
float --> "float"
...

来尝试一下


windows 环境的 __FUNCSIG__ 和类 Unix 系统的 __PRETTY_FUNCTION__,是两个可以打印函数的宏

看一下这段代码

#include <string_view>
#include <iostream>
using namespace std;

template<typename T>
constexpr std::string_view static_type_name() {
    return __PRETTY_FUNCTION__;
}

int main() {
    cout << static_type_name<int>() << endl;
}

输出结果是:

constexpr std::string_view static_type_name() [with T = int; std::string_view = std::basic_string_view<char>]

我们模板里填了一个 int,这个输出结果里也出现了一个 int

所以针对这个特性添油加醋:

(下面的代码根据我的试验结果做了修改)

template<typename T>
constexpr std::string_view static_type_name() {
#if _MSC_VER
    // ...
#else
    constexpr int skip_begin = 56;
    constexpr int skip_end   = 50;
    const char*   sig        = __PRETTY_FUNCTION__;
    return std::string_view(sig + skip_begin, strlen(sig) - skip_begin - skip_end);
#endif
}

然后取 static_type_name<int>() 得到的就是一个静态字符串 "int"

这还不够好,字符串是难以处理的,不妨将其用静态函数 hash 变成一个 uint32_t 整数

比如:

namespace detail {
  static constexpr uint32_t name_to_id_impl
      (const std::string_view& str, uint32_t hash) {
    if (str.size() == 0)
      return hash;

    for (auto i = 0; i < str.size(); ++i) {
      hash = SDBMHash(hash, str[i]);
    }
    
    return hash;
  }

  static constexpr uint32_t name_to_id(const std::string_view& str) {
    return name_to_id_impl(str, 0);
  }
} // namespace detail

template<typename T>
constexpr uint32_t static_type_id() {
    return detail::name_to_id(static_type_name<T>());
}

static_type_id<int>() 就能静态地得到 int 类型的字符串的 hash 结果,我们可以把它看成是跨平台的唯一类型 id 来使用


以上,all 所需的各种东西都准备齐全了

  • all 容易给实现一个 to 函数(方法和 all 的其他接口一致),它调用转换器管理类的接口获取转换器并使用
  • 我们事先给管理类注册转换器
    • 可以是自动生成的:conversion.add_converter(static_type_id<SRC>(), static_type_id<DST>(), type_converter<SRC, DST>::convert);
    • 其中 type_converter<SRC, DST>::convert 也可以换成其他自定义的 bool (*)(const void*, void*) noexcept 类型函数

这样,最初我们设想的

int b = a.to<int>();

就实现了!可喜可贺

all 容器的三个出发点:「是普通类」、「能存各种东西」、「能够类型转换」也都已完成


尾声

现存已有一些 cpp 反射库了,比如 Boost.Mirror 和 Qt

可能大部分时候反射的作用不大,但当遇到「序列化与反序列化」等场景,一个类型反射支持则尤为重要

所以虽然 cpp 对反射的包容性极差,实现一个比较可用的类型反射系统仍然是很有意义的

标签:容器,const,union,void,storage,c++,static,type,万物
From: https://www.cnblogs.com/Xiwon/p/18048567

相关文章

  • C++中cin的详细用法
    1.cin简介cin是C++编程语言中的标准输入流对象,即istream类的对象。cin主要用于从标准输入读取数据,这里的标准输入,指的是终端的键盘。此外,cout是流的对象,即ostream类的对象,cerr是标准错误输出流的对象,也是ostream类的对象。这里的标准输出指的是终端键盘,标准错误输出指的是终端的......
  • 基于WonderShaper对Docker容器进行带宽限速
    #安装WonderShaperaptupdateaptinstallwondershaper#创建Docker网桥dockernetworkcreate--driverbridge<网桥名>#然后终端会输出网桥ID:d0970005351xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxd30d0e757f0#记住前面几位然后查看所有网卡ifconfig#如果执行不了就自己去......
  • C++填坑系列——类型推导 decltype
    decltypedecltype主要是为了解决类型推导的问题,特别是在模板编程和泛型编程中应用较广泛。decltype关键字用于以表达式为参数,推导表达式返回的类型,该类型会保留所有信息。c++11提出的新特性,decltype关键字。和auto一样都是用来做编译时类型推导的,但是也有一些区别:auto:从......
  • C++填坑系列——左值和右值
    c++的表达式首先介绍下c++的表达式是什么?看下cppreference是怎么说的。Anexpressionisasequenceofoperatorsandtheiroperands,thatspecifiesacomputation.也就是说,在C++中,表达式(Expression)是由操作数(Operands)和运算符(Operators)组成的序列。左值和右值就是c++中......
  • C++填坑系列——lambda表达式
    lambda表达式总结:lambda表达式原理:被编译器转换为类+初始化对象的代码;格式:[captureslist](paramslist)specifiersexception->retType{funtionbody}按值捕获和按引用捕获的优缺点以及解决方法;一.lambda原理lambda函数会被编译器转换为类,并定义实现一个operato......
  • 压力容器、储气罐、压力容器焊接专业英语
    分类classification英文english中文chinesedefectapprovalofdefectinmaterial材料中缺陷的认可defectarccrater弧坑defectpenetration毛边defect不整齐的飞边修整defectcrack裂缝defectcrack-likeinperfection裂纹状缺陷defect......
  • C++ 类访问修饰符
    私有(private)成员成员和类的默认访问修饰符是private,如果没有使用任何访问修饰符,类的成员将被假定为私有成员。私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数......
  • Gitlab Runner自动执行Docker容器
    概述Gitlab完全可以执行dockerrun命令,本文用最简单的方式来演示。修改.gitlab-ci.yml加入第4个stage,运行dockerrun。stages:-build-docker-image-test-push-image-run-websitevariables:PAY_IMAGE_FULL_URL:docker.amihome.cn/amihome/chang......
  • C++类开发的第六篇(虚拟继承实现原理和cl命令的使用的bug修复)
    Class_memory接上一篇末尾虚拟继承的简单介绍之后,这篇来详细讲一下这个内存大小是怎么分配的。使用clcl是MicrosoftVisualStudio中的C/C++编译器命令。通过在命令行中键入cl命令,可以调用VisualStudio的编译器进行编译操作。cl命令提供了各种选项和参数,用于指定源......
  • C++ 把引用作为返回值
    通过使用引用来替代指针,会使C++程序更容易阅读和维护。C++函数可以返回一个引用,方式与返回一个指针类似。当函数返回一个引用时,则返回一个指向返回值的隐式指针。这样,函数就可以放在赋值语句的左边。例如,请看下面这个简单的程序:1#include<iostream>23usingnamesp......