为什么C ++没有reflection?

这是一个有点奇怪的问题。 我的目标是理解语言devise决策,并确定在C ++中反思的可能性。

  1. 为什么C ++语言委员会没有去执行语言反思? 在没有运行在虚拟机上的语言(如java)中,reflection太困难了?

  2. 如果有人为C ++实现反思,那么挑战是什么?

我想reflection的用途是众所周知的:编辑器可以更容易编写,程序代码会更小,可以为unit testing生成模拟等等。 但是,如果您也可以评论反思的用途,那将是非常好的。

在C ++中reflection有几个问题。

  • 要添加很多工作,C ++委员会是相当保守的,不要把时间花在激进的新function上,除非他们确信它会得到回报。 (有人build议增加一个类似于.NET程序集的模块系统,虽然我认为普遍的共识是,如果有,那不是他们当前的首要任务,而是一直推迟到很久以后C ++ 0x。这个function的动机是摆脱#include系统,但它也会启用至less一些元数据)。

  • 你不支付你不使用的东西。 这是C ++基础devise哲学之一。 为什么我的代码需要携带元数据,如果我可能永远不需要呢? 而且,元数据的添加可能会阻碍编译器的优化。 如果我可能永远不需要这些元数据,为什么我应该在代码中支付这些费用?

  • 这引出了另一个重要的一点:C ++对编译代码做出了很less的保证。 编译器可以做任何它喜欢的事情,只要产生的function是预期的。 例如,你的课程并不需要真正在那里 。 编译器可以对它们进行优化,内联它们所做的一切,而且它经常这样做,因为即使简单的模板代码也会创build很多模板实例。 C ++标准库依赖于这种积极的优化。 如果实例化和破坏对象的开销可以被优化,函子只是performance性的。 vector上的operator[]在性能上只能与原始数组索引相媲美,因为整个运算符可以内联,因此可以完全从编译代码中删除。 C#和Java对编译器的输出做了很多保证。 如果我用C#定义一个类,那么这个类将存在于生成的程序集中。 即使我从来不使用它。 即使所有对其成员函数的调用都可以被内联。 class必须在那里,以便反思能find它。 部分原因是C#编译为字节码,这意味着JIT编译器可以删除类定义和内联函数(如果它喜欢的话),即使最初的C#编译器不能。 在C ++中,只有一个编译器,并且必须输出高效的代码。 如果允许您检查C ++可执行文件的元数据,则希望看到它定义的每个类,这意味着编译器将不得不保留所有定义的类,即使它们不是必需的。

  • 然后有模板。 C ++中的模板与其他语言中的generics完全不同。 每个模板实例化都会创build一个新的types。 std::vector<int>是一个与std::vector<float>完全分离的类。 这在整个程序中加起来有很多不同的types。 我们的反思应该看到什么? 模板 std::vector ? 但是怎么可能,因为这是一个源代码构造,在运行时没有意义呢? 它必须看到单独的类std::vector<int>std::vector<float> 。 和std::vector<int>::iteratorstd::vector<float>::iterator ,对于const_iterator也是如此。 一旦你进入模板元编程,你很快就会实例化数以百计的模板,所有这些模板都被编译器内联和删除。 除了作为编译时元程序的一部分,它们没有任何意义。 这几百个课程是否应该反映出来? 他们不得不这样做,否则我们的反思将是无用的,如果它甚至不能保证我定义的类实际上在那里 。 另一个问题是模板类在实例化之前不存在。 想象一下使用std::vector<int> 。 我们的reflection系统应该能够看到std::vector<int>::iterator ? 一方面,你一定会这样想的。 这是一个重要的类,它是根据元数据存在的std::vector<int>定义的。 另一方面,如果程序从来没有真正使用这个迭代器类模板,那么它的types永远不会被实例化,所以编译器不会首先生成这个类。 在运行时创build它为时已晚,因为它需要访问源代码。

  • 最后,在C ++中reflection并不像C#那样重要。 原因是模板元编程。 它不能解决所有问题,但是在很多情况下,如果你想反思,可以编写一个元编程,在编译时做同样的事情。 boost::type_traits是一个简单的例子。 你想知道T型? 检查其type_traits 。 在C#中,你必须在使用reflection的types之后进行修改。 reflection对于某些东西(我可以看到,元编程不能轻易取代的主要用途,主要用于自动生成的序列化代码)仍然有用,但是对于C ++来说,它会带来一些重要的代价,而且它不是必须的是在其他语言。

编辑:回复评论:

cdleary:是的,debugging符号做类似的事情,因为它们存储有关在可执行文件中使用的types的元数据。 但他们也遭受我所描述的问题。 如果你曾经尝试过debugging发布版本,你会明白我的意思。 在源代码中创build一个类时存在很大的逻辑差距,在最终的代码中已经将其内联了起来。 如果你使用reflection来获得有用的东西,你需要它更加可靠和一致。 事实上,types几乎每次编译都会消失。 你改变了一个小小的细节,编译器决定改变哪些types被内联,哪些没有内联,作为响应。 当你甚至不能保证元数据中最相关的types将被表示时,你如何从中提取有用的东西? 你正在寻找的types可能在最后一个版本中,但现在已经消失了。 而明天呢,有人会把一个小小的无辜的变化检查到一个小的无辜的function,这个function使得这个types足够大,不会完全内联,所以它会再次回来。 这对于debugging符号仍然很有用,但是不仅仅如此。 我讨厌试图在这些条款下为一个类生成序列化代码。

Evan Teran:当然这些问题可以解决。 但是,这回落到我的观点#1。 这需要很多工作,C ++委员会有很多东西他们觉得更重要。 在C ++中获得一些有限的反思(这将是有限的)的好处是真的足够大,足以certificate以牺牲其他function为代价的重点? 是否真的有一个巨大的好处,增加function的核心语言已经(主要)通过图书馆和预处理器,如QT的? 也许,但是这种需求比没有这种图书馆的情况要less得多。 但是,对于您的具体build议,我相信在模板上禁用它会使其完全无用。 例如,您将无法在标准库上使用reflection。 什么样的reflection不会让你看到一个std::vector ? 模板是C ++的一个重要组成部分。 在模板上不起作用的function基本上是无用的。

但是你是对的,可以实施某种forms的反思。 但这将是语言的一个重大变化。 就目前而言,types完全是编译时构造。 它们是为了编译器而存在的,没有别的。 代码编译完成后,就没有类了。 如果你伸展自己,你可能会争辩说,function仍然存在,但实际上,所有有一堆跳转汇编指令,和大量的堆栈推/stream。 添加这样的元数据时,没有太多的事情要做。

但正如我所说,有一个build议修改编译模型,添加自包含的模块,为selecttypes存储元数据,允许其他模块引用它们,而不必与#include s #include 。 这是一个好的开始,说实话,我很惊讶标准委员会不会把这个提案放在太大的改变之外。 那么也许在5 – 10年内? 🙂

reflection需要一些关于types的元数据存储在可以查询的地方。 由于C ++编译为本地机器代码,并且由于优化而经历了巨大的变化,所以在编译过程中应用程序的高级视图几乎失去了,因此在运行时将无法查询它们。 Java和.NET在虚拟机的二进制代码中使用非常高级的表示,使得这种reflection级别成为可能。 然而,在一些C ++实现中,有一些称为运行时types信息(RTTI)的东西,它可以被认为是reflection的简化版本。

所有的语言都不应该试图把每一种语言的每个特征都包含进来。

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

针对不同的工作有不同的工具是很好的。 如果我们只有锤子,所有的东西都会像钉子一样。有脚本语言对于某些工作是有用的,而reflectionOO语言(Java,Obj-C,C#)对于另一类作业很有用,超级有效的裸机接近机器语言对于另一类作业(C ++,C,Assembler)很有用。

C ++在将汇编语言技术扩展到令人难以置信的复杂性pipe理水平方面做得非常出色,而抽象化使编程更大,更复杂的任务对人类来说更为可能。 但是,从严格的高层次angular度来看(Lisp,Smalltalk,Java,C#),这不一定是最适合那些正在接近他们的问题的语言。 如果您需要具备这些function的语言来最好地解决您的问题,那么请感谢那些为我们所有人创build了这些语言的人!

但是对于那些无论出于何种原因,C ++都需要在代码和底层机器操作之间有很强的关联的人来说。 无论是效率,还是编程设备驱动程序,还是与较低级别的操作系统服务(或其他)交互,C ++都更适合这些任务。

C#,Java,Objective-C都需​​要更大更丰富的运行时系统来支持它们的执行。 运行时必须交付给相关系统 – 预安装以支持您的软件的操作。 而且这个层必须被维护用于各种目标系统,通过一些其他语言来定制,以使其在该平台上工作。 而中间层 – 主机操作系统和你的代码之间的自适应层 – 运行时,几乎总是用C或C ++语言编写,其效率是#1,在那里可以很好地理解软件和硬件之间的确切交互理解,并操纵最大的收益。

我喜欢Smalltalk,Objective-C,并有一个丰富的运行时系统,包括reflection,元数据,垃圾回收等等。可以写出惊人的代码来利用这些设施! 但是,这只是堆栈中的一个更高层,一个层必须位于较低层,它们本身必须最终位于操作系统和硬件上。 我们将始终需要一种最适合构build该层的语言:C ++ / C / Assembler。

附录:C ++ 11/14正在继续扩展C ++支持更高级别抽象和系统的能力。 线程,同步,精确的内存模型,更精确的抽象机器定义使得C ++开发人员能够实现许多高级抽象,这些高级语言仅使用一些专有领域,而继续提供近距离的,金属性能和优异的可预测性(即最小的运行时子系统)。 也许反思设施将在未来的C ++修订版中有select性地启用,对于那些想要它的人 – 或者图书馆可能会提供这样的运行时服务(也许现在有一个,或者是一个提升的开始?)。

如果您真的想了解围绕C ++的devise决策,请查找Ellis和Stroustrup的“带注释的C ++参考手册”的副本。 它不符合最新的标准,但它通过了原来的标准,并解释了事情的工作方式,经常,他们是如何得到这种方式。

reflection可以和已经在c ++中实现。

这不是一个原生的c ++function,因为它的成本很高(内存和速度),不应该由语言默认设置 – 语言是“默认情况下的最大性能”。

因为你不应该支付你不需要的东西,就像你自己说的那样,在编辑器中比在其他应用程序中需要更多的东西,那么应该只在你需要的地方执行,而不是“强迫”所有的代码您不需要在编辑器或其他类似的应用程序中反映您将使用的所有数据)。

拥有它的语言的反思是编译器愿意在对象代码中留下多less源代码来启用reflection,以及有多less分析机器可用于解释reflection的信息。 除非编译器保留所有源代码,否则reflection在分析源代码可用事实的能力方面将受到限制。

C ++编译器不保留任何东西(好,忽略RTTI),所以你不会语言中得到reflection。 (Java和C#编译器只保留类,方法名和返回types,所以你得到一些reflection数据,但是你不能检查expression式或程序结构,这就意味着即使在那些“启用reflection”语言中你可以得到的信息是相当稀less的,因此你真的不能做很多的分析)。

但是你可以走出语言,获得全面的反思能力。 关于C中reflection的另一个堆栈溢出讨论的答案讨论了这一点。

C ++没有reflection的原因是,这将需要编译器将符号信息添加到对象文件中,例如类types的成员,成员信息,函数和所有内容。 这基本上会使包含文件无用,因为通过声明发送的信息然后将从那些对象文件(然后模块)中读取。 在C ++中,一个types定义可以通过包含相应的头文件(在所有这些定义相同的情况下)在程序中多次出现,因此必须决定将该types的信息放在哪里,就像命名一个并发症在这里。 C ++编译器所做的积极的优化,可以优化数十个类模板实例,是另一个优点。 这是可能的,但是由于C ++与C兼容,这将成为一个尴尬的组合。

在C ++中使用reflection的情况有很多,使用编译时结构(如模板元编程)无法充分解决。

N3340提出了丰富的指针,作为在C ++中引入reflection的一种方法。 除其他外,它解决了不支付function的问题,除非您使用它。

reflection可以是可选的,就像预处理指令一样。 就像是

#pragma enable reflection

那样的话,我们就可以拥有两全其美的世界,除了这个附注,图书馆可以毫无反思地创build(没有任何讨论的开销),那么无论他们想要的是速度还是易用性,这都是个体开发者的责任。

根据Alistair Cockburn的说法, 亚型不能在reflection环境中得到保证 。

反思与潜在的打字系统更相关。 在C ++中,你知道你有什么types,你知道你可以用它做什么。

如果C ++可以有:

  • variables名称,variablestypes和const修饰符的类成员数据
  • 函数参数迭代器(只有位置而不是名称)
  • 函数名称,返回types和const修饰符的类成员数据
  • 父类的列表(按照定义的顺序)
  • 模板成员和父类的数据; 扩展后的模板(这意味着实际的types可用于reflectionAPI,而不是“如何到达那里的模板信息”)

这足以在当今Web和数据库应用程序(所有的操作系​​统,消息传递机制,XML / JSONparsing器,数据序列化等)非常普遍的无types数据处理的关键中创build非常容易使用的库。

例如,由Q_PROPERTYmacros(Qt框架的一部分)支持的基本信息http://qt.nokia.com/doc/4.5/properties.html扩展到包含类方法和e); – 对C ++和到一般的软件社区。

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

在C ++reflection一些很好的链接我刚刚发现:

C ++标准工作文件:C ++中的reflection方面

使用模板进行reflection的简单示例

在C ++中反思,如果将C ++用作数据库访问,Web会话处理/ HTTP和GUI开发的语言,我相信这是至关重要的。 缺乏reflection防止ORM(如Hibernate或LINQ),XML和JSONparsing器实例化类,数据序列化和许多其他(最初无types的数据必须用于创build类的实例)。

在构build过程中,软件开发人员可以使用编译时间切换来消除这个“你付出什么使用”的顾虑。

我一个固件开发人员不需要reflection来从串口读取数据 – 那么很好,不要使用开关。 但是作为一个想要继续使用C ++的数据库开发人员,我总是会使用一个可怕的,难以维护的代码来在数据成员和数据库结构之间映射数据。

Boost系列化和其他机制都不能真正解决反思 – 它必须由编译器完成 – 一旦完成,C ++将再次在学校中使用,并用于处理数据处理的软件

对我来说这个问题#1(和基本的线程原语是问题#2)。

这基本上是因为它是一个“可选的额外”。 很多人selectC ++,而不是像Java和C#这样的语言,这样他们可以更好地控制编译器的输出,比如更小,更快的程序。

如果您select添加reflection,则可以使用各种解决scheme 。