为什么C++没有反射?

371

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

  1. C++语言委员会为什么没有朝着在语言中实现反射的方向发展?在不运行虚拟机(如java)的语言中,反射是否过于困难?

  2. 如果要为C ++实现反射,将面临哪些挑战?

我想反射的用途已经很清楚了:可以更容易地编写编辑器,程序代码将更小,可以为单元测试生成模拟对象等。但如果您能评论一下反射的用途,那就太好了。


3
因为我认为在C++中添加反射会让人发疯!;-) - KWallace
我同意KWallace的观点。其他编程语言已经很好地解决了这个问题:https://dev59.com/i4Hba4cB1Zd3GeqPODRX - Gabriel
我同意KWallace的观点。其他语言非常巧妙地解决了这个问题:https://stackoverflow.com/questions/24559016/delphi-use-reflection-in-a-class-procedure-for-the-getting-dynamic-class-type - undefined
15个回答

665

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等等。一旦您进入模板元编程,您很快就会实例化数百个模板,所有这些模板都会被编译器内联并删除。它们没有意义,除非作为编译时元程序的一部分。应该将这些数百个类全部显示给反射吗?他们必须这样做,否则我们的反射将是无用的,因为它甚至不能保证我定义的类实际上会“存在”。而一个副问题是,模板类在实例化之前不存在。想象一

    但是你说得对,可以实现某种形式的反射。但这将是语言中的重大变化。目前,类型是仅在编译时存在的构造。它们存在于编译器的好处,除此之外没有别的作用。一旦代码被编译,就没有类存在。如果你自己思考,你可能会认为函数仍然存在,但事实上,只有一堆跳转汇编指令和大量的栈推入/弹出。当添加这样的元数据时,可供参考的信息很少。

    但正如我所说,有一个更改编译模型的提案,添加自包含的模块,存储选定类型的元数据,允许其他模块引用它们而不必处理#include。这是一个良好的开端,老实说,我很惊讶标准委员会没有因为变化太大而放弃该提案。所以也许五到十年后呢? :)


  • 2
    这些问题大部分不是已经通过调试符号解决了吗?虽然由于内联和优化的原因它可能不会很高效,但你可以通过执行调试符号所做的操作来允许反射的可能性 - cdleary
    3
    关于你第一点的另一个问题:据我所知,还没有人尝试在C++实现中添加反射。这方面经验不足。委员会可能会不愿意担起领导责任,特别是在经历了exportvector<bool>之后。 - David Thornley
    24
    我同意C++不应该有运行时反射。但是编译时反射减少了一些上述问题,如果有人选择,可以用来构建特定类的运行时反射。能够通过模板访问类的第n个方法和第n个父类的类型、名称和特性,并在编译时获取这些数量? 这将使得基于CRTP的自动反射可行,同时不会浪费任何资源。 - Yakk - Adam Nevraumont
    21
    你的第三点在很多方面是最重要的:C++旨在适用于在内存费用昂贵的平台上编写独立代码;如果消除一些未使用的代码可以使程序适合成本为2.00美元的微控制器,而不是成本为2.50美元的微控制器,并且如果该代码将进入100万个单位,则消除该代码可以节省500,000美元。没有反思,静态分析通常可以识别90%以上的无法访问的代码;如果允许 Reflection,即使其中90% 不可达性,通过Reflection 可以到达的任何内容都必须被认为是可达的。 - supercat
    3
    委员会肯定可以轻松改进的一件事情是,最终明确表示typeinfoname()函数必须返回程序员键入的名称,而不是未定义的内容。并且还要为枚举器提供字符串化程序。这对于序列化/反序列化、帮助制造工厂等方面实际上非常重要。 - v.oddou
    显示剩余19条评论

    46

    反射需要存储一些类型元数据以便可以查询。由于C ++编译为本机机器代码,并且由于优化而发生了大量变化,因此在编译过程中丢失了应用程序的高级视图,因此无法在运行时查询它们。Java和.NET在虚拟机的二进制代码中使用非常高级别的表示,因此可以进行这种级别的反射。但是,在某些C++实现中,有一些称为运行时类型信息(RTTI)的内容,可以被认为是反射的简化版本。


    16
    RTTI是C++标准中的一部分。 - Daniel Earwicker
    1
    但并非所有的C++实现都是标准的。我见过一些不支持RTTI的实现。 - Mehrdad Afshari
    5
    大多数支持RTTI的实现也支持通过编译器选项关闭它。 - Michael Kohne
    我喜欢Delphi的反射(以及完整的RTTI支持):https://www.embarcaderoacademy.com/p/reflection-and-modern-rtti-in-delphi https://dev59.com/i4Hba4cB1Zd3GeqPODRX - Gabriel

    26

    所有编程语言不应尝试将每种其他编程语言的功能都整合进去。

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

    拥有不同的工具用于不同的工作是好的。 如果我们只有锤子,所有东西看起来都像钉子一样。 有脚本语言对于某些工作很有用,而反射式OO语言(Java,Obj-C,C#)对于另一类工作很有用,而超高效的近乎裸机语言对于另一类工作也很有用(C ++,C,汇编语言)。

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

    但是C ++是为那些因某种原因需要代码与基础机器操作之间有强烈关联的人设计的。 无论是效率,还是编写设备驱动程序,或者与低级别OS服务交互等,C ++都更适合这些任务。

    C#,Java,Objective-C都需要一个更大,更丰富的运行时系统来支持它们的执行。 那个运行时系统必须预先安装到相关系统中以支持您的软件的操作。 并且那个层次必须为各种目标系统进行定制,由某种其他语言编写,以使其在该平台上工作。 而那个中间层 - 主机OS和您的代码之间的自适应层 - 运行时几乎总是以效率为第一位的语言(如C或C ++),其中可以很好地理解和操纵软件与硬件之间的精确交互,以达到最大利益。

    我喜欢Smalltalk、Objective-C以及具有反射、元数据、垃圾回收等丰富运行时系统。利用这些功能可以编写出惊人的代码!但那只是在堆栈上的一个更高层次,必须建立在更低的层次之上,而它们自己最终必须坐落在操作系统和硬件之上。我们始终需要一种最适合构建该层的语言:C++/C/汇编。

    补充说明:C++11/14不断扩展C++支持更高级别的抽象和系统。线程、同步、精确内存模型、更精确的抽象机器定义使C++开发人员能够实现一些高级语言曾经独有的高级抽象,同时继续提供接近于底层的性能和优秀的可预测性(即最小化运行时子系统)。也许将来会有选择性地启用反射功能,供那些需要的人使用,或者一个库将提供这样的运行时服务(现在可能已经有了,或者在boost中已经开始了一个库)。


    3
    抱歉,只要你适当地链接它,你就可以用C语言编译Objective-C程序,事实上我在这里已经做到了:https://dev59.com/Emkv5IYBdhLWcg3w4kp2#10290255。你上面的陈述完全是错误的。运行时通过C语言是完全可访问的,这也是使其成为一种功能强大的动态语言之一的原因。 - Richard J. Ross III
    严谨的纠正:msvcrt提供了“C运行时库”,但本身并不是语言的核心。在某些时候,你需要考虑到程度:在运行时和非运行时之间如何划分界限?我认为C没有运行时 - 没有垃圾回收,没有其他线程管理任何东西,没有自动化。没有运行时类型系统。除了可以作为OS上的facade实现的malloc类型接口之外,什么都没有。因此,除非您认为OS本身是“语言运行时”,否则C根本没有运行时。 - Mordachai
    实际上你可以:操作系统提供了mmap,从这个意义上说,它为语言提供了一个分配器。有时它也支持sbrk,以及堆栈页面承诺、堆栈保护页面访问信号、分段错误信号、sigbus、管道创建、线程创建、filber创建、上下文切换支持、文件系统抽象、管道等等... 对我来说,这是一个巨大的运行时。 - v.oddou
    1
    “C运行时库”只是一个包含C标准库代码的动态库。同样,“C++运行时库”也是如此。这与Objective-C的运行时系统非常不同。虽然我想你理论上可以在C中使用Objective-C运行时,但那仍然只是一个使用Objective-C运行时的C程序 - 你不能用C编译一个真正的Objective-C程序。 - celticminstrel
    2
    C++11具有内存模型和原子性,使其更像可移植的汇编语言。这些不是高级别的东西,它们是C++以前缺乏可移植支持的低级别的东西。但如果你做错了什么,在C++中的未定义行为很多,这使它非常不像基于虚拟机的语言如Java,也不像任何特定的汇编语言。例如,C++源代码中的有符号溢出完全是未定义行为,即使编译为x86,编译器也可以基于此进行优化,但在几乎所有平台上的汇编中它只会回绕。现代C++与可移植汇编语言相差甚远。 - Peter Cordes
    显示剩余7条评论

    12
    如果你真的想要理解有关C++设计决策的内容,可以找到Ellis和Stroustrup所著的The Annotated C++ Reference Manual一书。虽然它不是最新标准,但它详细介绍了原始标准,并解释了事物的工作方式,以及它们通常是如何变得这样的。

    9
    C++的设计与演化,作者为Stroustrup。 - James Hopkin

    11

    对于支持反射的编程语言来说,编译器愿意在目标代码中保留多少源代码以启用反射,以及可以解释反射信息的分析机制有多少决定了反射的能力。如果编译器没有保留所有源代码,那么反射将受到限制,无法分析源代码的所有可用信息。

    C++编译器不会保留任何内容(忽略RTTI),因此您无法在语言中获得反射。(Java和C#编译器仅保留类、方法名称和返回类型,因此您可以获得一些反射数据,但无法检查表达式或程序结构,这意味着即使在这些“启用反射”的语言中,您可以获取的信息也非常有限,因此您实际上无法进行太多分析)。

    但是,您可以跨越语言界限获得完整的反射能力。关于在C语言中实现反射的问题,另一个Stack Overflow讨论的答案 提供了相关信息


    7

    在c++中,反射已经被实现过。

    它不是c++的本地特性,因为它会带来沉重的代价(内存和速度),这不应该成为语言默认的设置 - 该语言是“默认最大性能”导向的。

    正如你不应该为你不需要的东西付费一样,在编辑器等其他应用程序中更需要它,那么它应该只在需要它的地方实现,并且不会“强制”到所有代码中(在编辑器或其他类似应用程序中,您不需要对所有要处理的数据进行反射)。


    3
    你不应该把代码符号发送出去,因为这会让你的客户/竞争对手看到你的代码...这通常被认为是一件坏事。 - gbjbaanb
    你说得对,我甚至没有考虑到代码曝露的问题 :) - Klaim

    6
    在过去的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"有点玩具化,但您可以编写QObject、Reflectable、PolymorphicValueType或NetworkProtocol元类,以修改类定义的含义。
    这可能会涉及到。它继续变得更好,但也继续被推迟。对于大多数主要的C++编译器,都有多个编译时反射实现可供尝试。语法正在不断变化,因为有基于符号运算符的反射库、基于reflexpr的运算符反射库,一些反映数据是类型,其他则是constexpr对象和consteval函数。

    6
    C++没有反射的原因是这需要编译器向目标文件添加符号信息,例如类类型的成员,成员信息,函数等。这将使包含文件无用,因为声明提供的信息将从这些对象文件(模块)中读取。在C++中,通过包括相应的头文件,可以在程序中多次出现类型定义(前提是所有这些定义都相同),因此必须决定放置有关该类型的信息,这只是其中一个复杂问题。C++编译器进行的激进优化可以优化掉数十个类模板实例化,这也是另一个强项。虽然可能实现,但由于C++兼容C,因此这将变成一种笨拙的组合。

    1
    我不明白编译器的激进优化如何成为一个强项。你能详细说明一下吗?如果链接器可以删除重复的内联函数定义,那么重复的反射信息有什么问题呢?符号信息不是已经被添加到目标文件中供调试器使用了吗? - Rob Kennedy
    1
    问题在于您的反射信息可能无效。如果编译器消除了80%的类定义,那么您的反射元数据会怎么样呢?在C#和Java中,语言保证一旦定义了一个类,它就会一直存在。而C++则允许编译器对其进行优化。 - jalf
    1
    @Rob,优化是另一个问题,与多类复杂性无关。请参考@jalf的评论(以及他的答案)了解我的意思。 - Johannes Schaub - litb
    4
    如果我实例化了 reflect<T>,那么不要丢弃任何 T 的信息。这似乎不是一个无法解决的问题。 - Joseph Garvin

    4

    更一般地说,能够检查不存在的特征而不引入未定义行为的能力,使得将该特征添加到类的后续版本中可能会改变现有程序的明确定义行为,并因此使得无法保证添加该特征不会“破坏”某些东西。 - supercat
    @supercat 老评论,老问题:但是SFINAE在C++11中使这个问题存在。我猜9年前它并不是很出名! - Yakk - Adam Nevraumont
    @Yakk-AdamNevraumont:至少应该存在一个区别,即使不能保证某种结构对所有组件的组合都能正常工作,也不应该破坏它对于实际组合的有效性。然而,有些人似乎将不强制正确操作当作破坏事物的邀请。 - supercat

    4

    有很多情况下需要在C++中使用反射,而这些情况无法通过编译时构造(如模板元编程)充分解决。

    N3340 提出了丰富指针作为引入C++反射的一种方式。其中,它解决了只有在使用时才需要付费的问题,同时还有其他功能。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接