这是一篇组会分享,并且是拖了很长很长时间的那种。这次不会再鸽了
这篇文章可以说是针对某 cpp 佬的公众号的两篇原创内容的笔记
什么是反射
这个好像没有严格的定义,但是概括的说,「反射」是指在程序运行期对程序本身进行访问和修改的能力
比如以下一些例子:
-
我们希望通过一个字符串
"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 T
用T
存储- 其他的保持不变
这样对于类型 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