深入探讨C++的高级反射机制

news2024/9/20 16:53:14

反射是一种编程语言能力,允许程序在运行时查询和操纵对象的类型信息。它广泛应用于对象序列化、远程过程调用、测试框架、和依赖注入等场景。
由于C++语言本身的反射能力比较弱,因此C++生态种出现了许多有趣的反射库和实现思路。我们在本文一起探讨其中的奥秘。

反射实现类型

高级反射的两种实现思路分别是编译时反射运行时反射

编译时反射

编译时反射 (Compile-time Reflection) 在C++中通常依赖模板元编程来实现。这种反射在编译时确定类型信息,不需要动态类型识别 (RTTI)。这种方法的优点在于可以生成性能更优的代码,减小二进制文件的大小,因为所有的类型信息在编译时都已确定,不需要在运行时查询。

优点
  • 性能:由于类型信息在编译时就已确定,可以避免运行时查找,从而提高性能。
  • 二进制大小:不需要存储额外的类型信息,可以减小最终二进制文件的大小。
  • 确定性:编译时反射的结果在编译完成后就已确定,这给程序的行为带来了确定性。
缺点
  • 维护成本:需要手动注册每个需要反射的类型和成员,增加了代码的维护难度。
  • 灵活性较差:程序一旦编译完成,无法改变其反射的行为。
实现原理

在C++中,编译时反射通常利用模板特化和宏定义来实现类型注册。
https://github.com/Ubpa/USRefl/tree/master库就是一个典型的编译时反射的库。
编译时反射库的使用往往需要入侵源码,下面是一个简单的使用TypeInfo特化来注册类型信息的示例:

struct Point {
  float x, y;
};

template<>
struct TypeInfo<Point> : TypeInfoBase<Point> {
  static constexpr FieldList fields = {
    Field { "x", &Point::x },
    Field { "y", &Point::y }
  };
};

在这个例子中,我们为Point类型特化了TypeInfo模板类,定义了一个静态的fields字段列表,其中包含了Point结构体的成员变量。
下面是上面的编译时反射的使用示例,它演示了如何遍历Point结构体的字段:

TypeInfo<Point>::fields.ForEach([](const auto& field) {
  std::cout << field.name << std::endl;
});

如果需要不入侵源码,还有一种做法是通过代码预处理技术实现生成反射的类型信息,使用这种技术实现反射最著名的莫过于Qt的元反射机制和元对象编译器MOC。

Qt的反射机制

代码预处理技术通过预处理步骤生成或修改源代码,从而实现反射。
Qt框架通过一个称为Meta-Object Compiler (MOC)的元对象编译器来提供反射能力。MOC是一个代码预处理器,它在C++编译之前运行,扫描源代码中的特殊宏(如Q_OBJECTsignalsslots),并生成额外的C++代码,这些代码包含了类型信息和用于信号与槽机制的元信息。

例如,如果一个类需要使用Qt的信号和槽机制,则必须在类定义中包含Q_OBJECT宏:

#include <QObject>

class MyClass : public QObject {
  Q_OBJECT
public:
  MyClass(QObject* parent = nullptr);
  virtual ~MyClass();

signals:
  void mySignal();

public slots:
  void mySlot();
};

MOC会识别Q_OBJECT宏,并为MyClass生成额外的C++代码文件,这个文件包含了反射需要的元信息。这些信息允许在运行时查询类的信号和槽,以及进行信号和槽之间的连接。

使用Qt的MOC技术,开发者可以在运行时执行类似如下的动态操作:

MyClass myObject;
QMetaObject::invokeMethod(&myObject, "mySlot");

上述代码将在运行时调用mySlot槽函数,而不需要在编译时知道该槽的存在。

代码预处理的优势和挑战

代码预处理技术的优势在于它能够在不改变C++语言本身的情况下实现反射。这种方法灵活且与编译器无关,可以跨平台使用。

然而,这种技术也有其挑战和缺点:

  • 额外的构建步骤:需要在编译前运行预处理器,这使得构建过程更复杂。
  • 开发工具的兼容性:一些集成开发环境(IDE)和代码编辑器可能需要特殊配置或插件来支持这种预处理步骤。
  • 额外的学习成本:开发者需要学习额外的宏和注解方式,这增加了学习和使用框架的难度。

虽然C++标准不直接支持反射,但通过编译器扩展和代码预处理技术,开发者们仍然能够在C++中实现类似反射的功能。这些技术在实践中证明了其有效性,并在许多项目中得到了成功的应用。

运行时反射

运行时反射 (Runtime Reflection) 是许多动态语言(如Python、Java和C#)的标准功能。C++的RTTI提供了有限的运行时反射能力,例如通过typeiddynamic_cast获取类型信息和进行类型转换。

优点

  • 灵活性:可以在程序运行时查询和操纵类型信息,为动态行为提供了可能。
  • 自动化:大多数支持运行时反射的语言会自动处理类型信息的注册和管理。

缺点

  • 性能开销:运行时查询类型信息需要时间,可能会影响性能。
  • 二进制大小:需要存储额外的类型信息,增加了二进制文件的大小。

实现原理

运行时反射依靠语言运行时系统在内存中维护类型信息。在C++中,RTTI提供了typeid操作符来获取对象的类型信息:

Point p;
std::cout << typeid(p).name() << std::endl;

使用示例

在Java中,运行时反射的使用示例可能如下所示:

Class<?> clazz = Class.forName("java.lang.String");
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
    System.out.println(method.getName());
}

C++为什么不直接支持一流的反射

C++作为一种静态类型语言,它的设计哲学强调性能和内存控制。直接支持运行时反射将违背这种设计哲学,因为运行时反射需要在内存中维护类型信息的数据结构,这会增加额外的内存和性能开销。

此外,C++的编译模型和链接模型也不适合直接支持反射。C++程序通常由多个翻译单元组成,它们在链接时才最终形成一个程序。这使得在编译时跨翻译单元维护类型信息变得困难。

C++的未来发展趋势

C++社区和标准委员会正在探索如何在未来的标准中增加反射的支持。最新的C++标准已经包含了一些反射相关的提案,比如静态反射,这表明C++正逐步朝着增强其反射能力的方向发展。

最后,一起实现一个最简单的C++编译时反射功能吧

编写一个最简单的C++编译时反射库涉及到模板元编程和一些宏定义。下面是一个非常基础的版本,这个反射库仅支持遍历字段名称和获取字段值。

#include <iostream>
#include <tuple>
#include <stdexcept>
#include <assert.h>
#include <string_view>
#include <optional>
#include <utility> // For std::forward

namespace refl {

	// 这个宏用于创建字段信息
#define REFLECTABLE_PROPERTIES(TypeName, ...)  using CURRENT_TYPE_NAME = TypeName; \
    static constexpr auto properties() { return std::make_tuple(__VA_ARGS__); }
#define REFLECTABLE_MENBER_FUNCS(TypeName, ...) using CURRENT_TYPE_NAME = TypeName; \
    static constexpr auto member_funcs() { return std::make_tuple(__VA_ARGS__); }

// 这个宏用于创建属性信息,并自动将字段名转换为字符串
#define REFLEC_PROPERTY(Name) refl::Property<decltype(&CURRENT_TYPE_NAME::Name), &CURRENT_TYPE_NAME::Name>(#Name)
#define REFLEC_FUNCTION(Func) refl::Function<decltype(&CURRENT_TYPE_NAME::Func), &CURRENT_TYPE_NAME::Func>(#Func)

// 定义一个属性结构体,存储字段名称和值的指针
	template <typename T, T Value>
	struct Property {
		const char* name;
		constexpr Property(const char* name) : name(name) {}
		constexpr T get_value() const { return Value; }
	};
	template <typename T, T Value>
	struct Function {
		const char* name;
		constexpr Function(const char* name) : name(name) {}
		constexpr T get_func() const { return Value; }
	};

	// 用于获取特定成员的值的函数,如果找不到名称,则返回默认值
	template <typename T, typename Tuple, size_t N = 0, size_t RetTypeSize = 0>
	constexpr void* __get_field_value_impl(T& obj, const char* name, const Tuple& tp) {
		if constexpr (N >= std::tuple_size_v<Tuple>) {
			return nullptr;
		}
		else {
			const auto& prop = std::get<N>(tp);
			if (std::string_view(prop.name) == name) {
				assert(RetTypeSize == sizeof(prop.get_value()));// 返回值类型传错了
				return (void*)&(obj.*(prop.get_value()));
			}
			else {
				return __get_field_value_impl<T, Tuple, N + 1, RetTypeSize>(obj, name, tp);
			}
		}
	}

	template <typename RetType, typename T, typename Tuple, size_t N = 0>
	constexpr RetType* get_field_value(T& obj, const char* name, const Tuple& tp) {
		return (RetType*)__get_field_value_impl<T, Tuple, N, sizeof(RetType)>(obj, name, tp);
	}

	// 成员函数相关:
	template <typename RetType, typename T, typename FuncTuple, size_t N = 0, typename... Args>
	constexpr std::optional<RetType> __invoke_member_func_impl(T& obj, const char* name, const FuncTuple& tp, Args&&... args) {
		if constexpr (N >= std::tuple_size_v<FuncTuple>) {
			//throw std::runtime_error(std::string(name) + " Function not found."); // 可以选择抛出异常或者通过optional判断是否成功
			return std::optional<RetType>();
		}
		else {
			const auto& func = std::get<N>(tp);
			if (std::string_view(func.name) == name) {
				return (obj.*(func.get_func()))(std::forward<Args>(args)...);
			}
			else {
				return __invoke_member_func_impl<RetType, T, FuncTuple, N + 1>(obj, name, tp, std::forward<Args>(args)...);
			}
		}
	}

	template <typename RetType, typename T, typename... Args>
	constexpr std::optional <RetType> invoke_member_func(T& obj, const char* name, Args&&... args) {
		constexpr auto funcs = T::member_funcs();
		return __invoke_member_func_impl<RetType>(obj, name, funcs, std::forward<Args>(args)...);
	}


	// 定义一个类型特征模板,用于获取属性信息
	template <typename T>
	struct For {
		static_assert(std::is_class_v<T>, "Reflector requires a class type.");

		// 遍历所有字段名称
		template <typename Func>
		static void for_each_propertie_name(Func&& func) {
			constexpr auto props = T::properties();
			std::apply([&](auto... x) {
				((func(x.name)), ...);
				}, props);
		}

		// 遍历所有字段值
		template <typename Func>
		static void for_each_propertie_value(T& obj, Func&& func) {
			constexpr auto props = T::properties();
			std::apply([&](auto... x) {
				((func(x.name, obj.*(x.get_value()))), ...);
				}, props);
		}

		// 遍历所有函数名称
		template <typename Func>
		static void for_each_member_func_name(Func&& func) {
			constexpr auto props = T::member_funcs();
			std::apply([&](auto... x) {
				((func(x.name)), ...);
				}, props);
		}
	};

}// namespace refl


// =========================一下为使用示例代码====================================

// 用户自定义的结构体,需要反射的字段使用REFLECTABLE宏来定义
struct MyStruct {
	int x{ 10 };
	float y{ 20.5f };
	int print() const {
		std::cout << "MyStruct::print called! " << "x: " << x << ", y: " << y << std::endl;
		return 666;
	}

	REFLECTABLE_PROPERTIES(MyStruct,
		REFLEC_PROPERTY(x),
		REFLEC_PROPERTY(y)
	);
	REFLECTABLE_MENBER_FUNCS(MyStruct,
		REFLEC_FUNCTION(print)
	);
};

int main() {
	MyStruct obj;

	// 打印所有字段名称
	refl::For<MyStruct>::for_each_propertie_name([](const char* name) {
		std::cout << "Field name: " << name << std::endl;
		});

	// 打印所有字段值
	refl::For<MyStruct>::for_each_propertie_value(obj, [](const char* name, auto&& value) {
		std::cout << "Field " << name << " has value: " << value << std::endl;
		});

	// 打印所有函数名称
	refl::For<MyStruct>::for_each_member_func_name([](const char* name) {
		std::cout << "Member func name: " << name << std::endl;
		});

	// 获取特定成员的值,如果找不到成员,则返回默认值
	auto x_value = get_field_value<int>(obj, "x", MyStruct::properties());
	std::cout << "Field x has value: " << *x_value << std::endl;

	auto y_value = get_field_value<float>(obj, "y", MyStruct::properties());
	std::cout << "Field y has value: " << *y_value << std::endl;

	auto z_value = get_field_value<int>(obj, "z", MyStruct::properties()); // "z" 不存在
	std::cout << "Field z has value: " << z_value << std::endl;

	// 通过字符串调用成员函数 'print'
	auto print_ret = refl::invoke_member_func<int>(obj, "print");
	std::cout << "print member return: " << print_ret.value() << std::endl;

	return 0;
}


这个反射库用到了折叠表达式,因此需要支持C++17的编译器才能正常编译。
编译运行后,可以看到结构体的名称被正确的显示出来:
在这里插入图片描述

这个编译时反射库非常基础,只支持非静态数据成员,并且每个字段必须手动注册。在实际应用中,一个成熟的编译时反射库会更复杂,支持更多功能,如类型信息查询、继承关系处理等。但是,我们通过这个例子,可以更久深入地理解C++的编译时反射的实现原理和技术细节,非常有趣。
针对这个库进一步完善,可以参考文章:深入探讨C++的高级反射机制(2):写个能用的反射库

扩展知识:关于C++的折叠表达式

我们前面提到,由于用到了折叠表达式,需要支持C++17的编译器才能正常编译。那么什么折叠表达式呢?
C++的折叠表达式(Fold Expression)是C++17标准引入的一种新特性,它允许对一个包含了所有参数的参数包进行一个二元操作的展开。折叠表达式可以简化有关变参模板函数的编写,例如上面我们需要对所有的变参执行某项操作时。

折叠表达式有两种形式:一元右折叠和一元左折叠。它们分别用 (... op pack)(pack op ...) 表示,其中 op 是一个二元运算符,pack 是一个参数包。C++17也支持二元折叠表达式 (init op ... op pack)(pack op ... op init)

以下是一些折叠表达式的例子:

template<typename... Args>
auto sum(Args... args) {
    return (... + args); // 一元右折叠,将参数包中所有元素求和
}

template<typename... Args>
auto logical_and(Args... args) {
    return (true && ... && args); // 二元左折叠,逻辑与操作
}

template<typename... Args>
bool all_positive(Args... args) {
    return ((args > 0) && ...); // 一元右折叠,判断所有参数是否都大于0
}

在第一个例子中,(... + args) 是一种右折叠表达式。如果传给 sum 函数的参数是 (1, 2, 3),折叠表达式的展开将是 1 + (2 + 3)

在第二个例子中,true && ... && args 是一种左折叠表达式。如果传的参数是 (a, b, c),那么展开将是 true && a && b && c

第三个例子是一种右折叠表达式,它检查所有参数是否都大于0。如果传的参数是 (1, 2, 3),那么展开将是 1 > 0 && 2 > 0 && 3 > 0

折叠表达式极大简化了变参模板代码的编写,使得对参数包的操作更加直接和清晰。在C++17之前,要对参数包中的所有元素进行操作通常涉及到递归模板实例化或使用初始化列表的技巧来实现,这相对来说更加复杂且代码可读性较差。

结语

如果你耐心的读到这里,相信你对C++的编译时反射的原理和实现都有了更深入的认识,以后再做C++反射相关的事情,也会更加游刃有余了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1877715.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入解析内容趋势:使用YouTube API获取视频数据信息

一、引言 YouTube&#xff0c;作为全球最大的视频分享平台之一&#xff0c;汇聚了无数优质的内容创作者和观众。从个人分享到专业制作&#xff0c;从教育科普到娱乐休闲&#xff0c;YouTube上的视频内容丰富多彩&#xff0c;满足了不同用户的需求。对于内容创作者、品牌以及希…

langchain学习总结

大模型开发遇到的问题及langchain框架学习 背景&#xff1a; 1、微场景间跳转问题&#xff0c;无法实现微场景随意穿插 2、大模型幻读&#xff08;推荐不存在的产品、自己发挥&#xff09; 3、知识库检索&#xff0c;语义匹配效果较差&#xff0c;匹配出的结果和客户表述的…

nacos 整合 openfeign实现远程调用

结合之前写过的案例 Springcloud Alibaba nacos简单使用 Springcloud 之 eureka注册中心加feign调用 在微服务架构中&#xff0c;服务注册与发现是一个关键组件。Nacos 是一个开源的服务注册与发现、配置管理平台&#xff0c;而 OpenFeign 是一个声明式的 Web 服务客户端&am…

【智能算法】目标检测算法

目录 一、目标检测算法分类 二、 常见目标检测算法及matlab代码实现 2.1 R-CNN 2.1.1 定义 2.1.2 matlab代码实现 2.2 Fast R-CNN 2.2.1 定义 2.2.2 matlab代码实现 2.3 Faster R-CNN 2.3.1 定义 2.3.2 matlab代码实现 2.4 YOLO 2.4.1 定义 2.4.2 matlab代码实现…

eBPF技术揭秘:DeepFlow如何引领故障排查,提升运维效率

DeepFlow 实战&#xff1a;eBPF 技术如何提升故障排查效率 目录 DeepFlow 实战&#xff1a;eBPF 技术如何提升故障排查效率 微服务架构系统中各个服务、组件及其相互关系的全景 零侵扰分布式追踪&#xff08;Distributed Tracing&#xff09;的架构和工作流程 关于零侵扰持…

力扣 单链表元素删除解析及高频面试题

目录 删除元素的万能方法 构造虚拟头结点来应对删除链表头结点的情况 一、203.移除链表元素 题目 题解 二、19.删除链表中倒数第K个节点 题目 题解 三、 83.删除某个升序链表中的重复元素&#xff0c;使重复的元素都只出现一次 题目 题解 82.删除某个升序链表中的…

【UML用户指南】-23-对高级行为建模-状态机

目录 1、概述 2、状态 2.1、状态的组成 3、转移 3.1、转移的组成 4、高级状态和转移 4.1、进入效应和退出效应 4.2、内部转移 4.3、do活动 4.4、延迟事件 4.5、子状态机 5、子状态 5.1、非正交子状态 5.2、历史状态 5.3、正交子状态 6、分叉与汇合 7、主动对象…

【摄像头标定】双目摄像头标定及矫正-opencv(python)

双目摄像头标定及矫正 棋盘格标定板标定矫正 棋盘格标定板 本文使用棋盘格标定板&#xff0c;可以到这篇博客中下载&#xff1a;https://blog.csdn.net/qq_39330520/article/details/107864568 标定 要进行标定首先需要双目拍的棋盘格图片&#xff0c;20张左右&#xff0c;…

【最简单】解决windows安装wsl,出现WslRegisterDistribution failed with error: 0x8007019e的问题

从官网下载安装包安装ubuntu18.04的过程中出现了下面的错误 在Windows上安装Windows Subsystem for Linux (WSL) 时&#xff0c;可能会遇到以下错误&#xff1a; WslRegisterDistribution failed with error: 0x8007019e 这个错误通常是由于系统未启用必要的功能或未正确配置…

计算机网络微课堂(湖科大教书匠)TCP部分

计算机网络微课堂&#xff08;湖科大教书匠&#xff09;TCP部分 【计算机网络微课堂&#xff08;有字幕无背景音乐版&#xff09;】 TCP的流量控制 一般来说&#xff0c;我们希望数据传输得更快一些。但如果发送方把数据发送得过快&#xff0c;接收方就可能来不及接收&#…

【Vue】Vue3基础

VUE3基础 1、简介2、创建工程2.1 基于vue-cli创建&#xff08;脚手架webpack&#xff09;2.2 基于vite创建&#xff08;推荐&#xff09;2.3 目录结构2.4 vscode插件推荐 3、核心语法3.1 选项式&#xff08;options API&#xff09;和组合式&#xff08;composition API&#x…

json文件 增删查改

默认收藏夹 qt操作json格式文件... 这个人的 写的很好 我的demo全是抄他的 抄了就能用 —————————— 下次有空把我的demo 传上来 在E盘的demo文件夹 json什么名字

「ETL趋势」FDL数据开发支持版本管理、实时管道支持多对一、数据源新增支持神通

FineDataLink作为一款市场上的顶尖ETL工具&#xff0c;集实时数据同步、ELT/ETL数据处理、数据服务和系统管理于一体的数据集成工具&#xff0c;进行了新的维护迭代。本文把FDL4.1.8最新功能作了介绍&#xff0c;方便大家对比&#xff1a;&#xff08;产品更新详情&#xff1a;…

Profinet IO从站数据 转EtherCAT项目案例

这里是引用 目录 1 案例说明 1 2 VFBOX网关工作原理 1 3 准备工作 2 4 使用PRONETA软件获取PROFINET IO从站的配置信息 2 5 设置网关采集PROFINETIO从站设备数据 5 6 启动ETHERCAT从站转发采集的数据 8 7 选择槽号和数据地址 9 8 选择子槽号 11 9 案例总结 12 1 案例说明 设置…

《昇思25天学习打卡营第12天 | 昇思MindSpore基于MindSpore的GPT2文本摘要》

12天 本节学习了基于MindSpore的GPT2文本摘要。 1.数据集加载与处理 1.1.数据集加载 1.2.数据预处理 2.模型构建 2.1构建GPT2ForSummarization模型 2.2动态学习率 3.模型训练 4.模型推理

揭秘Etched AI:三个哈佛辍学00后挑战英伟达,推出Transformer专用ASIC芯片sohu

人工智能领域最近掀起了一股新的热潮&#xff0c;三位哈佛辍学的00后本科生创建了Etched AI&#xff0c;并成功推出了一款超强AI芯片sohu&#xff0c;直指英伟达的AI芯片帝国。这款芯片被誉为比英伟达H100快20倍&#xff0c;吸引了众多科技界的关注。本文将深入探讨Etched AI及…

五、Spring IoCDI ★ ✔

5. Spring IoC&DI 1. IoC & DI ⼊⻔1.1 Spring 是什么&#xff1f;★ &#xff08;Spring 是包含了众多⼯具⽅法的 IoC 容器&#xff09;1.1.1 什么是容器&#xff1f;1.1.2 什么是 IoC&#xff1f;★ &#xff08;IoC: Inversion of Control (控制反转)&#xff09;总…

带上作弊器,我不得起飞

前言 过去,我们对人工智能既期待又害怕.人类的惰性希望人工智能可以帮助大家从大部分繁重的工作中解放出来,但又害怕它失控. 智能系统的好处 工作方面 自动化与效率提升&#xff1a;可以自动执行许多重复性和低技能的任务&#xff0c;如制造业中的装配、数据输入和办公室的客户…

java的序列化和反序列化

一、概念 序列化是将对象的常态存储到特定的存储介质中的过程。 反序列化是将特定的存储介质中的数据重新构建对象的过程。 问题 为每个对象属性——编写读写代码&#xff0c;过程很繁琐且非常容易出错&#xff0c;如何解决&#xff1f; 二、使用Object Output Stream类实现…

敏捷开发笔记(第9章节)--开放-封闭原则(OCP)

目录 1&#xff1a;PDF上传链接 9.1 开放-封闭原则&#xff08;OCP&#xff09; 9.2 描述 9.3 关键是抽象 9.3.1 shape应用程序 9.3.2 违反OCP 糟糕的设计 9.3.3 遵循OCP 9.3.4 是的&#xff0c;我说谎了 9.3.5 预测变化和“贴切的”结构 9.3.6 放置吊钩 1.只受一次…