编译时计算一直是C++引以为傲的能力之一。从最初的模板元编程,到C++11的constexpr,再到C++20的constexpr容器操作和C++23的constexpr标准库扩展,C++在“将更多工作移至编译期”的道路上不断前进。这条道路的终极目标是编译时反射——在编译期获取类型的信息,并基于这些信息生成代码。如果这个目标完全实现,C++将获得类似其他语言中宏或代码生成器的能力,但更加类型安全、更加强大。
参考:https://vrhyh.cn/category/siji.html
constexpr函数在C++11中首次亮相,但受到严格限制:只能包含一条return语句,不能有局部变量,不能有循环。C++14放宽了限制,允许局部变量、if语句和循环,使得constexpr函数看起来与普通函数几乎无异。C++17进一步允许constexpr的lambda表达式。C++20带来了革命性的变化:constexpr函数现在可以使用std::vector和std::string,只要这些操作在编译期完成,并且最终结果不包含指向非constexpr内存的指针。
这意味着什么?这意味着你可以编写一个constexpr函数,在编译期读取一个字符串,进行复杂的解析和转换,返回一个std::vector,然后将这个vector作为编译期常量使用。所有这些都在编译期完成,运行时开销为零。一个经典的例子是编译期正则表达式:在编译期解析正则表达式,生成一个高效的匹配引擎,然后在运行时用这个引擎匹配文本。传统上,这种工作需要在运行时解析正则表达式,或使用外部代码生成器。现在,纯C++就可以完成。
编译期容器是这一趋势的自然延伸。虽然std::vector可以在constexpr上下文中使用,但它有一个限制:不能作为非类型模板参数传递。C++20允许某些类类型作为非类型模板参数,但要求类型是“结构体类型”(类似于C的聚合体),这排除了std::vector。未来的标准可能放宽这一限制,允许将编译期容器作为模板参数传递,从而实现真正的编译期编程。
参考:https://vrhyh.cn/category/xinli.html
编译期字符串一直是constexpr的痛点。字符串字面量在C++中是常量字符数组,可以作为模板参数传递(通过将字符串编码为字符序列),但语法极其笨拙。C++20允许将字符数组作为非类型模板参数,这使得编译期字符串处理变得更加简单。但完全支持std::string作为编译期类型仍然需要解决分配器的问题——如何在编译期分配和释放内存?
编译期类型信息是constexpr无法直接提供的。要获取一个类型的名称、成员列表、基类列表、以及访问控制信息,我们需要反射。C++26反射提案(目前是TS状态)定义了编译期查询类型信息的机制。例如,你可以写constexpr auto members = reflexpr(MyStruct)::members;,然后在编译期遍历这些成员,自动生成序列化函数、哈希函数、比较运算符等。
反射与constexpr的结合开启了代码生成的新纪元。传统上,C++中的代码生成需要外部工具(如protobuf、Qt的moc、或各种代码生成器)。这些工具工作得很好,但它们破坏了构建流程的简洁性,并且生成的代码往往难以调试。有了编译期反射,你可以在同一个文件中写反射代码,在编译期生成所需要的函数,无需任何外部工具。
参考:https://vrhyh.cn/category/yundong.html
考虑一个具体的场景:你有一个包含许多字段的结构体,想要为它实现operator==。手工编写这个运算符是繁琐且容易出错的(当添加新字段时容易忘记更新)。使用反射,你可以编写一个通用的equality函数模板,它在编译期遍历结构体的所有成员,逐个比较。这个函数可以放在头文件中,对所有类型自动生效。
同样地,序列化也可以基于反射实现。一个serialize函数可以遍历结构体的所有成员,依次写入输出流。反序列化函数则按照相同的顺序读取。这种自动生成的序列化代码虽然可能不是最优的(例如,你可能需要控制成员顺序或跳过某些成员),但对于大多数情况已经足够。
反射的另一个重要应用是编译期单元测试。你可以编写一个测试框架,在编译期运行测试,如果测试失败,编译器产生错误信息。这比运行时测试更早地发现问题,并且不会增加最终二进制文件的大小。
C++23的std::is_within_lifetime是一个小但重要的功能:它允许在编译期检查一个指针是否指向一个生命周期内的对象。这对于实现安全的自引用类型和编译期检查器至关重要。
静态反射与动态反射的区别值得注意。动态反射(如Java、C#中的反射)在运行时查询类型信息,性能较差,且需要保留元数据(增加二进制大小)。静态反射在编译期完成所有工作,运行时没有开销,但灵活性较低——你无法在运行时查询一个未知类型的信息,因为所有信息都在编译期就决定了。
C++社区对反射的设计存在分歧。一派主张“值反射”——反射信息作为编译期值,可以使用constexpr函数处理。另一派主张“类型反射”——反射信息作为新的类型,使用模板元编程处理。目前的主流方向是值反射,因为它与constexpr的集成更好,且更符合现代C++的风格。
反射的标准化进程可能还需要几年。C++26可能包含初级的反射能力,如枚举反射(获取枚举值的名称和数量)和类成员反射(获取成员列表)。完整的反射(包括基类、访问控制、注解等)可能要到C++29或更晚。
对于普通开发者来说,反射的到来意味着什么?它意味着许多样板代码可以自动生成。你不再需要为每个类编写operator==、operator<<、哈希函数、序列化函数。你不再需要手动维护访问者模式或双重派发的代码。你不再需要担心当添加新字段时忘记更新某些函数。反射将把开发者从这些重复性劳动中解放出来,让他们专注于真正的业务逻辑。
当然,反射也不是银弹。过度使用反射可能导致编译时间爆炸(因为编译器需要在编译期进行大量计算),也可能导致代码难以理解(因为代码的行为不再显式地写在源代码中)。像所有强大的工具一样,反射需要谨慎使用。
参考:https://vrhyh.cn