为什么c++没有反射?

这是个有点奇怪的问题。我的目标是理解语言设计决策,并确定在c++中反射的可能性。

  1. 为什么c++语言委员会没有在语言中实现反射?在不运行在虚拟机上的语言(如java)中反射是否太困难了?

  2. 如果要在c++中实现反射,会遇到什么挑战?

我想反射的用途是众所周知的:编辑器可以更容易地编写,程序代码将更小,可以为单元测试生成模拟等等。但是如果你能评论一下反射的用法就太好了。

64601 次浏览

反射需要一些关于类型的元数据存储在可以查询的地方。由于c++编译为本机机器代码,并且由于优化而经历了大量更改,因此在编译过程中应用程序的高层视图几乎丢失了,因此,在运行时不可能查询它们。Java和. net在虚拟机的二进制代码中使用非常高级的表示,使得这种级别的反射成为可能。然而,在一些c++实现中,有一种叫做运行时类型信息(RTTI)的东西,它可以被认为是反射的简化版本。

c++没有反射的原因是,这将要求编译器向目标文件添加符号信息,比如类类型的成员,关于成员的信息,关于函数的信息等等。这实际上会使包含文件变得无用,因为通过声明传递的信息将从这些目标文件(然后是模块)中读取。在c++中,一个类型定义可以通过包含各自的头文件在程序中出现多次(前提是所有这些定义都是相同的),因此必须决定在哪里放置关于该类型的信息,就像在这里命名一个复杂性一样。由c++编译器完成的积极优化是另一个优点,它可以优化出几十个类模板实例化。这是可能的,但由于c++与C兼容,这将成为一个尴尬的组合。

如果你真的想了解c++的设计决策,找一份Ellis和Stroustrup的注释c++参考手册。它并不是最新的标准,但它贯穿了最初的标准,并解释了事情是如何工作的,以及它们是如何实现的。

在c++中有几个关于反射的问题。

  • 要添加的工作很多,c++委员会是相当保守的,除非他们确定会有回报,否则不会花时间在激进的新特性上。(有人建议添加一个类似于。net程序集的模块系统,虽然我认为大家普遍认为这样做很好,但这不是他们目前的首要任务,而且已经被推迟到c++ 0x之后。这个特性的动机是摆脱#include系统,但它也将启用至少一些元数据)。

  • 不劳而获 使用。这是必备的基本要素之一 c++的设计理念。 为什么我的代码应该随身携带 元数据,如果我可能永远不需要它? 此外,元数据的添加 可以抑制编译器从 优化。我为什么要付那么多钱 成本在我的代码中,如果我可能永远不需要 元数据?李< / p > < / > 这就引出了另一个重要问题: c++很少保证非常 关于编译后的代码。的 编译器被允许做漂亮 它喜欢什么都行,只要 结果的功能是什么 预计。例如,你的 实际上并不要求课程 在那里。编译器可以对它们进行内联优化 他们所做的一切 经常这样做,因为 即使是简单的模板代码也会 创建一些模板 实例化。c++标准 库依赖在这个侵略性 优化。函子只是 性能如果开销 实例化和销毁 对象可以被优化。 vector上的operator[]只能与raw相比 性能中的数组索引 因为整个算子都可以 内联,因此完全删除 从编译的代码。c#和Java 做出很多保证 编译器的输出。如果我定义 一个c#中的类,那么class 将 在结果程序集中存在。 即使我从来不用它。即使所有 可以调用其成员函数 内联。这门课必须 在那里,这样反射才能找到 它。c#可以部分缓解这一问题 编译为字节码,这意味着 JIT编译器可以删除 类定义和内联 如果它喜欢,函数,即使 初始的c#编译器不能。在c++中, 你只有一个编译器,而它 必须输出有效的代码。如果你 可以检查元数据吗 一个c++可执行文件,你会期望 查看它定义的每个类 意味着编译器会 为了保存所有已定义的类, 即使它们不是必需的。

  • 然后是模板。 c++中的模板与此完全不同 其他语言中的泛型。每一个 模板实例化创建 类型。std::vector<int>是一个完全独立的类 std::vector<float>。这加起来就是 一个整体有很多不同的类型 程序。我们应该反思什么 看到了吗?模板 std::vector?但 怎么可能,因为那是 源代码构造,其中没有 是指在运行时?这得看 单独的类 std::vector<int>std::vector<float>。和 std::vector<int>::iteratorstd::vector<float>::iterator,同样的 const_iterator等等。和 一旦你进入模板 元编程,你很快就会结束 实例化数百个模板, 所有这些都被内联并删除 还是由编译器。他们没有 意思是,除了作为 编译时元程序。都应该 这数百个类是可见的 反射?他们必须这么做, 否则我们的反射 如果它甚至不能保证我定义的类实际上会在那里,那么它将是无用的。另一个问题是模板类在实例化之前并不存在。想象一个使用std::vector<int>的程序。我们的反射系统是否能够看到std::vector<int>::iterator?一方面,你肯定会这么想。它是一个重要的类,它是根据std::vector<int>定义的,而存在于元数据中。另一方面,如果程序从未真正使用这个迭代器类模板,它的类型将永远不会被实例化,因此编译器将不会在第一时间生成该类。在运行时创建它就太晚了,因为它需要访问源代码

  • 最后,反射不是完全 它在c++和c#中一样重要。的 理由也是模板 元编程。它不能解决 一切,但在很多情况下 否则你会求助于 反射,可以写一个 元程序做同样的事情 在编译时。 boost::type_traits是一个简单的 的例子。你想知道类型 T吗?检查它的type_traits。在c#中, 你得在它之后到处找 使用反射类型。反射 对某些人来说还是有用的 (我能看到的主要用途是, 哪一个元编程不容易 替换,是为自动生成 序列化代码),但它会 进行一些重大的成本 在c++中,它只是不像在其他语言中那样经常需要

< em >编辑: 在回应评论:

< p > cdleary: 是的,调试符号也有类似的功能,因为它们存储关于可执行文件中使用的类型的元数据。但是他们也遭受我所描述的问题。如果您曾经尝试过调试发布版本,您就会明白我的意思。在源代码中创建类时存在很大的逻辑间隙,而在最终代码中已经内联了该类。如果要将反射用于任何有用的地方,则需要它更加可靠和一致。实际上,几乎每次编译时,类型都会消失。你改变一个很小的细节,编译器决定改变哪些类型得到内联,哪些没有,作为响应。当您甚至不能保证在元数据中表示最相关的类型时,如何从中提取有用的内容呢?您正在寻找的类型可能在上一个版本中存在,但现在它消失了。明天,有人会签入一个无害的小变化到一个无害的小函数,这使得类型足够大,它不会完全内联,所以它会再次回来。这对于调试符号仍然很有用,但仅此而已。我讨厌尝试在这些术语下为类生成序列化代码 Evan Teran:当然这些问题是可以解决的。但这又回到了我的第一点。这需要做很多工作,c++委员会有很多他们认为更重要的事情。在c++中获得一些有限的反射(它将是有限的)的好处真的大到足以以牺牲其他特性为代价来关注它吗?在核心语言中添加功能真的有很大的好处吗?这些功能已经(大部分)可以通过库和像QT这样的预处理器来实现了。也许吧,但如果没有这样的图书馆,这种需求就没有那么迫切了。 至于你的具体建议,我认为在模板上禁用它会使它完全无用。例如,您将无法在标准库上使用反射。什么样的反射不会让你看到std::vector?模板是c++的巨大的部分。在模板上不起作用的特性基本上是无用的

但你是对的,某种形式的反射是可以实现的。但这将是语言上的一个重大变化。就像现在一样,类型只是一个编译时构造。它们的存在是为了编译器的利益,而不是别的。一旦代码编译完成,就没有类了。如果你扩展自己,你可能会认为函数仍然存在,但实际上,所有的只是一堆跳转汇编指令,以及大量的堆栈推送/弹出。在添加这样的元数据时,没有什么可做的。

但就像我说的,有一个修改编译模型的建议,添加自包含的模块,为选择的类型存储元数据,允许其他模块引用它们而不必乱动__abc0。这是一个很好的开始,说实话,我很惊讶标准委员会没有因为这个改变太大而把这个提议否决掉。所以也许在5-10年后?:)

对于具有反射的语言来说,它是关于编译器愿意在目标代码中保留多少源代码来启用反射,以及有多少分析机制可用来解释反射的信息。除非编译器保留所有源代码,否则反射分析源代码可用事实的能力将受到限制。

c++编译器不会在周围保留任何东西(好吧,忽略RTTI),所以你不会得到反射语言。(Java和c#编译器只保留类、方法名和返回类型,所以你只能得到一点点反射数据,但你不能检查表达式或程序结构,这意味着即使在那些“启用反射”的语言中,你能得到的信息也非常少,因此你真的不能做很多分析)。

但是你可以step 语言并获得完整的反射功能。C反射中另一个堆栈溢出讨论的答案讨论了这个问题。

我相信,如果c++要用作数据库访问、Web会话处理/http和GUI开发的语言,那么c++中的反射是至关重要的。缺乏反射阻碍了orm(如Hibernate或LINQ)、实例化类的XML和JSON解析器、数据序列化和许多其他东西(最初必须使用无类型数据来创建类的实例)。

可以使用软件开发人员在构建过程中可用的编译时开关

.

.

我是一个固件开发人员,不需要反射来从串口读取数据——那么很好,不使用交换机。但是作为一个想要继续使用c++的数据库开发人员,我经常要面对一个可怕的、难以维护的代码,这些代码在数据成员和数据库结构之间映射数据。

无论是Boost序列化还是其他机制都不能真正解决反射问题——它必须由编译器来完成——一旦完成,c++将再次在学校中教授,并用于处理数据处理的软件中

对我来说,这是问题#1(而原生线程原语是问题#2)。

如果c++可以:

  • 类成员数据用于变量名、变量类型和const修饰符
  • 函数参数迭代器(只有位置而不是名称)
  • 类成员数据用于函数名、返回类型和const修饰符
  • 父类列表(与定义的顺序相同)
  • 模板成员和父类的数据;扩展的模板(意味着实际的类型将可用于反射API,而不是“如何到达那里的模板信息”)
这足以在无类型数据处理的关键处创建非常容易使用的库,而无类型数据处理在今天的web和数据库应用程序中非常普遍 (所有的orm,消息传递机制,xml/json解析器,数据序列化等) 例如,Q_PROPERTY宏(Qt框架的一部分)支持的基本信息 http://qt.nokia.com/doc/4.5/properties.html扩展到涵盖类方法和e) -将对c++和一般的软件社区非常有益

当然,我所指的反射不会涵盖语义或更复杂的问题(如注释、源代码行号、数据流分析等)——但我也不认为这些是语言标准的一部分。

根据Alistair Cockburn, 在反射环境中不能保证子类型

反射与潜在类型系统更相关。在c++中,你知道你得到了什么类型,你知道你可以用它做什么。

所有的语言都不应该试图融合其他语言的所有特征。

c++本质上是一个非常非常复杂的宏汇编器。它不是(传统意义上的)c#、Java、Objective-C、Smalltalk等高级语言。

对于不同的工作有不同的工具是很好的。如果我们只有锤子,所有东西看起来都像钉子。拥有脚本语言对于某些作业是有用的,而具有反射性的oo语言(Java, Obj-C, c#)对于另一类作业是有用的,而超级高效的基本的接近机器的语言对于另一类作业是有用的(c++, C, Assembler)。

c++在将汇编技术扩展到令人难以置信的复杂管理和抽象方面做了一项了不起的工作,使人类编程更大、更复杂的任务变得更有可能。但它并不一定是最适合那些从严格的高级角度(Lisp, Smalltalk, Java, c#)来处理问题的语言。如果您需要一种具有这些特性的语言来最好地实现您的问题的解决方案,那么感谢那些为我们所有人创建这些语言的人!

但c++是为那些出于某种原因,需要在代码和底层机器操作之间建立强相关性的人准备的。无论是它的效率,还是编程设备驱动程序,还是与底层操作系统服务的交互,或者其他什么,c++都更适合这些任务。

c#、Java、Objective-C都需要一个更大、更丰富的运行时系统来支持它们的执行。运行时必须交付给有问题的系统-预安装以支持您的软件的操作。这一层必须为各种目标系统进行维护,由某些其他语言定制,以使其在该平台上工作。而中间层——宿主操作系统和你的代码之间的自适应层——运行时,几乎总是用C或c++这样的语言编写的,在这些语言中,效率是第一位的,可以很好地理解软件和硬件之间的精确交互,并获得最大收益。

我喜欢Smalltalk、Objective-C,以及拥有一个包含反射、元数据、垃圾收集等的丰富运行时系统。可以编写令人惊叹的代码来利用这些设施!但这只是堆栈上的一个更高的层,它必须依赖于更低的层,而这些层最终必须依赖于操作系统和硬件。我们总是需要一种最适合构建这一层的语言:c++ /C/Assembler。

附录:c++ 11/14将继续扩展c++的能力,以支持更高级别的抽象和系统。线程、同步、精确的内存模型、更精确的抽象机器定义使c++开发人员能够实现许多高级抽象,而这些高级抽象是一些高级语言曾经独占的领域,同时继续提供接近于硬件的性能和出色的可预测性(即最小的运行时子系统)。也许在c++的未来版本中,对于那些需要它的人,会有选择地启用反射功能——或者有一个库将提供这样的运行时服务(也许现在就有一个,或者在boost中开始有一个?)

在c++中使用反射的情况有很多,而使用模板元编程等编译时结构无法充分解决这些问题。

N3340建议将富指针作为c++中引入反射的一种方式。除此之外,它还解决了一个问题,那就是除非你使用某个功能,否则就不用为它付费。

反射可以是可选的,就像预处理器指令一样。类似的

#pragma enable reflection

通过这种方式,我们可以两全其美,没有这个pragma库就可以在没有反射的情况下创建(没有任何开销),然后就由个人开发人员决定他们想要的是速度还是易用性。

这基本上是因为它是一个“可选的额外项目”。许多人选择c++而不是Java和c#等语言,这样他们可以更好地控制编译器的输出,例如,一个更小和/或更快的程序。

如果你选择添加反射,有可用的各种解决方案

c++是一种不需要反射的语言,因为c++是一种可以用来编写具有反射的语言的语言。

在过去的10年里,人们一直在尝试向c++中添加反射。最新的提案是,可能会,也可能不会。

与大多数语言中的反射不同,反射的计划是编译时反射。所以在编译时,你可以反射结构成员、函数和方法参数和属性、枚举值和名称等。

然后,您可以进行有限的具体化,注入关于反射的信息以生成其他类型和代码。

虽然这有点奇怪,但这意味着不使用反射的程序不会为它支付运行时成本。它也非常强大。

最简单的例子是,您可以使用它来实现运行时反射。

struct Member {
std::string_view name;
std::any_ref value;
};


struct Reflectable {
virtual std::span<Member> GetMembers() const = 0;
virtual std::span<Member> GetMembers() = 0;
};


template<class D>
struct ImplReflectable:Reflectable {
std::span<Member> GetMembers() const final;
std::span<Member> GetMembers() final;
};
template<class D>
std::span<Member> ImplReflectable<D>::GetMembers() const {
// compile time reflection code on D here
}
template<class D>
std::span<Member> ImplReflectable<D>::GetMembers() {
// compile time reflection code on D here
}

你把上面的代码写了一次,突然你就可以对任何你想要反射的类型,你可以这样做:

struct Point : ImplReflectable<Point> {
int x, y;
};

并且一个反射系统被附加到Point

实现此运行时反射的库可以像您喜欢的那样复杂和强大。每种类型都必须做一些工作(如上所述)才能选择加入,但对于UI库(例如)这样做并不是一个严重的问题。没有选择的类型继续c++的假设“如果你不使用它,就不为它付费”。

但这仅仅是个开始。一个提议,元类,允许:

interface Reflectable {
std::span<Member> GetMembers() const;
std::span<Member> GetMembers();
};

您可以使用元类或接受类型并返回类型的函数。这允许你定义类的元类,比如用语言编写的“interface”。现在,interface有点像玩具,但你可以编写QObjectReflectablePolymorphicValueTypeNetworkProtocol元类来修改类定义的含义。

这可能会也可能不会进入。它会继续变得更好,但也会继续被推回去。对于大多数主要的c++编译器,您可以尝试多种编译时反射实现。语法是不断变化的,因为有基于符号操作符的反射库,基于reflexpr的操作符反射库,其中一些反射数据是类型,另一些是constexpr对象和consteval函数。