为什么C++没有反射?

ami*_*mit 326 c++ reflection

这是一个有点离奇的问题.我的目标是理解语言设计决策并确定C++中反射的可能性.

  1. 为什么C++语言委员会不会在语言中实现反思?对于不在虚拟机上运行的语言(如java),反射是否太难?

  2. 如果要实现C++的反射,那么挑战是什么?

我想反射的使用是众所周知的:编辑器可以更容易编写,程序代码更小,可以为单元测试生成模拟等等.但是,如果你也可以对反射的使用发表评论,那就太棒了.

jal*_*alf 612

C++中的反射有几个问题.

  • 这需要做很多工作,C++委员会相当保守,除非他们确定能够获得回报,否则不要花时间研究激进的新功能.(建议添加一个类似于.NET程序集的模块系统,虽然我认为普遍的共识是它很高兴,但它现在不是他们的首要任务,并且一直被推迟到C++ 0x.此功能的动机是摆脱#include系统,但它也会启用至少一些元数据).

  • 你不支付你不使用的东西.这是C++背后必须的基本设计理念之一.如果我可能永远不需要,我的代码为什么要携带元数据?此外,元数据的添加可能会阻止编译器进行优化.如果我可能永远不需要那些元数据,为什么我要在代码中支付这笔费用?

  • 这使我们再大点:C++做对编译的代码很少保证.只要产生的功能符合预期,编译器就可以做任何喜欢的事情.例如,您的课程不需要实际 存在.编译器可以优化它们,内联它们所做的一切,并且经常这样做,因为即使是简单的模板代码也往往会创建相当多的模板实例.C++标准库依赖于这种积极的优化.如果可以优化实例化和破坏对象的开销,则函数仅具有性能. operator[]在向量上仅与性能中的原始数组索引相当,因为整个运算符可以内联并因此完全从已编译的代码中删除.C#和Java对编译器的输出做了很多保证.如果我在C#中定义一个类,那么该类将存在于生成的程序集中.即使我从不使用它.即使可以内联所有对其成员函数的调用.班级必须在那里,以便反思可以找到它.部分结果通过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++带来一些重要的成本,并且它不需要像它一样频繁.是用其他语言.

编辑: 回应评论:

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

Evan Teran:当然这些问题可以解决.但这又回到了我的观点#1.它需要做很多工作,C++委员会有很多他们认为更重要的事情.在C++中得到一些有限的反思(并且它会受到限制)的好处是否真的足以证明以牺牲其他功能为代价来关注它?添加功能核心语言是否真的有很大的好处,这些功能已经(大部分)通过库和预处理器(如QT)完成了?也许,但是,如果不存在这样的图书馆,那么需求就不那么紧迫了.但是,对于您的具体建议,我认为在模板上禁止它会使它完全无用.例如,您无法在标准库上使用反射.什么样的反思不会'std::vector?模板是C++ 的重要组成部分.对模板不起作用的功能基本上没用.

但你是对的,可以实施某种形式的反思.但这是语言的一个重大变化.就像现在一样,类型只是一个编译时构造.它们的存在是为了编译器的利益,而不是其他任何东西.一旦代码被编译,也没有课.如果你伸展自己,你可能会认为函数仍然存在,但实际上,所有的都是一堆跳转汇编指令,以及大量的堆栈推送/弹出.添加此类元数据时,没有太多事情可做.

但正如我所说,有一个改进编译模型的建议,添加自包含模块,存储选择类型的元数据,允许其他模块引用它们而不必乱用#includes.这是一个良好的开端,说实话,我很惊讶标准委员会并没有因为太大的改变而抛出提案.也许在5到10年内?:)

  • 我同意C++不应该有运行时反映.但是编译时反射几乎没有上述问题,并且可以用于某些人如果他们选择在特定类上构建运行时反射.能够通过模板访问第n个方法的类型,名称和功能以及类的第n个父级吗?并在编译时得到这样的数量?会使基于CRTP的自动反射变得可行,而没有人为他们不使用的东西付费. (17认同)
  • 您的第三点在很多方面都是最重要的:C++旨在适合在内存成本高昂的平台上编写独立代码; 如果消除一些未使用的代码将允许程序适合价格为2.00美元的微控制器,而不是价格为2.50美元的微控制器,并且如果代码是1,000,000单位,则消除该代码可以节省500,000美元.如果没有反射,静态分析通常可以识别90%以上的无法访问的代码; 如果允许反射,那么任何可以通过反射到达的东西都必须被推定为可达,即使其中90%不是. (10认同)
  • 大多数这些问题是否必须通过调试符号来解决?并不是说它会是高性能的(因为你提到的内联和优化),但是你可以通过做任何调试符号来实现反射的*可能性*. (2认同)
  • 关于你的第一点的另一件事:据我所知,没有人试图在C++实现中添加反射.它没有很好的经验.委员会可能不愿意带头,特别是在`export`和`vector <bool>`之后. (2认同)
  • 肯定有一些东西可以被comitee轻松改进,最后说白色的黑色是`typeinfo`的`name()`函数必须返回程序员键入的名字而不是未定义的名字.并为我们提供一个枚举器字符串.这对序列化/反序列化至关重要,有助于制作工厂等. (2认同)

Meh*_*ari 38

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

  • RTTI符合C++标准. (14认同)
  • 大多数支持RTTI的实现也支持通过编译器选项将其关闭. (3认同)

Mor*_*hai 17

所有语言都不应该尝试包含所有其他语言的所有功能.

C++本质上是一个非常非常复杂的宏汇编程序.它不是(传统意义上的)高级语言,如C#,Java,Objective-C,Smalltalk等.

为不同的工作提供不同的工具是很好的.如果我们只有锤子,那么所有的东西都会看起来像指甲等.有了脚本语言对某些工作很有用,反射的OO语言(Java,Obj-C,C#)对另一类工作很有用,而且超级 - 高效的裸机接近机器语言对于另一类作业(C++,C,汇编程序)非常有用.

C++在将Assembler技术扩展到令人难以置信的复杂度管理水平方面做得非常出色,而且抽象使得编程更大,更复杂的任务对于人类来说更加可能.但对于那些从严格的高级角度(Lisp,Smalltalk,Java,C#)处理问题的人来说,它不一定是最适合的语言.如果您需要具有这些功能的语言来最好地实现问题的解决方案,那么感谢那些为我们所有人创建这些语言的人!

但是C++适用于那些无论出于何种原因需要在代码和底层机器操作之间建立强关联的人.无论是效率,编程设备驱动程序,还是与低级OS服务或其他任何内容的交互,C++都更适合这些任务.

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

我喜欢Smalltalk,Objective-C,并且拥有丰富的运行时系统,包括反射,元数据,垃圾收集等.可以编写出色的代码来利用这些功能!但这只是堆栈上的一个更高层,一层必须停留在较低层,它们本身必须最终位于操作系统和硬件上.我们将始终需要一种最适合构建该层的语言:C++/C/Assembler.

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

  • 对不起; 但是只要你正确地链接它,你就可以在C中编译一个objective-c程序,实际上我在这里做了:http://stackoverflow.com/a/10290255/427309.您的上述全部陈述均属错误.运行时可以通过C完全访问,它是使其成为如此强大的动态语言的东西之一. (3认同)
  • 具有内存模型+原子的C ++ 11使它*更多*像便携式汇编程序一样。这些不是高级的东西,它们是C ++以前缺乏可移植支持的*低*级别的东西。但是,如果您做错了任何事情,C ++中的UB数量将使其与Java等基于VM的语言非常不同,并且与任何特定的汇编语言也不相同。例如,签名溢出在C ++源代码中完全是UB *并且编译器可以基于该事实进行优化*即使编译为x86,但在几乎所有平台上的asm中,它都将环绕。现代C ++与可移植的汇编语言相距甚远。 (2认同)

Mic*_*hne 11

如果您真的想了解围绕C++的设计决策,请查找Ellis和Stroustrup 的The Annotated C++ Reference Manual的副本.它不是最新的标准,但它符合原始标准,并解释了事情的工作原理和经常,他们是如何做到这一点的.

  • 还有Stroustrup的C++设计和演变 (6认同)

Ira*_*ter 9

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

C++编译器不会保留任何东西(好吧,忽略RTTI),所以你不会语言中得到反射.(Java和C#编译器只保留类,方法名和返回类型,所以你得到一些反射数据,但你不能检查表达式或程序结构,这意味着即使在那些"反射启用"语言中你可以获得的信息非常稀少,因此你真的无法进行太多的分析).

但是,你可以一步之外的语言,并得到充分反映能力.关于C中反射的另一个堆栈溢出讨论的答案讨论了这一点.


Kla*_*aim 8

可以并且已经在c ++中实现了反射.

它不是本机c ++特性,因为它具有很高的成本(内存和速度),默认情况下不应该由语言设置 - 语言是"默认的最大性能".

因为你不应该为你不需要的东西买单,而且你自己说它在编辑器中比在其他应用程序中需要的更多,那么它应该只在你需要的地方实现,而不是"强制"到所有的代码(您不需要在编辑器或其他类似应用程序中反映您将使用的所有数据.

  • 而且你不发货符号因为它会让你的客户/竞争对手看你的代码...这通常被认为是一件坏事. (3认同)

Yak*_*ont 7

在过去的 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
}
Run Code Online (Sandbox Code Playgroud)

你写了一次上面的内容,然后突然你对于任何你想要反射的类型,你可以这样做:

struct Point : ImplReflectable<Point> {
  int x, y;
};
Run Code Online (Sandbox Code Playgroud)

并附有反射系统Point

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

但这只是开始。一项提案,元类,允许:

interface Reflectable {
  std::span<Member> GetMembers() const;
  std::span<Member> GetMembers();
};
Run Code Online (Sandbox Code Playgroud)

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

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


Joh*_*itb 6

C++没有反射的原因是,这需要编译器将符号信息添加到目标文件中,例如类类型的成员,有关成员的信息,有关函数和所有内容的信息.这基本上会使包含文件无用,因为声明传递的信息将从那些目标文件(模块然后)中读取.在C++中,类型定义可以在程序中多次出现,包括相应的头文件(前提是所有这些定义都相同),因此必须决定在何处放置有关该类型的信息,就像命名一个复杂在这里.由C++编译器完成的积极优化可以优化数十个类模板实例化,这是另一个优点.这是可能的,但由于C++与C兼容,

  • 如果我实例化反映<T>,那么不要丢弃任何T的信息.这似乎不是一个无法解决的问题. (4认同)