纯虚函数不能有内联定义。为什么?

65

纯虚函数是指那些具有虚拟性并带有纯虚拟限定符(= 0;)的成员函数。

C++03标准第10.4节第2段告诉我们什么是抽象类,并且作为一个附注,以下内容:

[注意:函数声明不能同时提供纯虚限定符和定义—结尾注释][例如:

struct C {
virtual void f() = 0 { }; // ill-formed
};

—end example]

对于那些不太熟悉此问题的人,请注意纯虚函数可以有定义,但是上述规定禁止这样的定义内联(在类中按词法顺序)。 (您可以查看定义纯虚函数的用途,例如这篇 GotW 文章

对于所有其他类型的函数,都可以提供内联定义,在类内也是允许的。这个限制乍一看似乎非常人为和莫名其妙。仔细一想,第二次甚至更多次观察时,它似乎也是如此 :) 但我相信如果没有特定的原因,就不会有这种限制。

我的问题是:有没有人知道这些具体的原因?欢迎提出好的猜测。

注:

  • MSVC 允许将 PVF(pure virtual functions) 定义为内联。所以不要惊讶:)
  • 本问题中的单词 inline 不是指 inline 关键字。它应该意味着 按词法顺序在类中

1
使用函数try块时看起来有点奇怪:virtual void f() = 0 try { } catch(...) { } - Johannes Schaub - litb
2
这是一个猜测:创建一个不允许它的编译器更简单,考虑到它非常罕见的使用情况,这是一个容易的决定。一些编译器(你引用了MSVC)允许它的事实仅仅意味着一些编译器作者并不介意额外的工作。 - Tergiver
7
@Downvoter: 这个问题是否主观且有争议?是否不是一个真正的问题?格式/表述是否不佳?请告诉我,这样我就可以改进以符合您的高标准。 - Armen Tsirunyan
1
当然它更“难”。这是一个额外的符号,必须允许在函数声明结构上。诚然,这并不是非常困难,但是无论价值大小,未完成的工作都是节省时间的。无论如何,我并没有说这是一个好猜测,只是一个猜测。 - Tergiver
1
@Armen,我花了一些时间来思考这个问题,也许我对这个问题太过敏感了:我仍然不喜欢带有提供实现的纯虚函数。因此,我真的很喜欢GotW将他们的文章命名为**(Im)pure Virtual Functions**。 :-) 然而,如果我碰巧遇到这种情况,我很高兴知道它的存在。对于这个问题点赞。 - Stephane Rolland
显示剩余16条评论
5个回答

54
在SO线程"为什么纯虚函数可以用0来初始化?"中,Jerry Coffin引用了Bjarne Stroustrup的C++设计与演化第13.2.3节的一句话,我加重了我认为相关的部分:
“当时选择了奇怪的=0语法而不是引入新的关键字pure或abstract,是因为我认为没有机会得到新关键字的接受。如果我建议使用pure,Release 2.0就会发布没有抽象类。在更好的语法和抽象类之间做出选择,我选择了抽象类。我使用传统的C和C++约定,使用0表示“不存在”,而不是冒险延迟并遭受关于pure的争执。 =0语法符合我的观点,即函数体是函数的初始化程序,也符合(简单但通常足够的)将虚函数集实现为函数指针向量的视图[...]”。
因此,在选择语法时,Bjarne认为函数体是声明符的一种初始化程序,=0是一种替代的初始化程序形式,它表示“没有函数体”(或用他的话说,“不存在”)。
在那个概念上,一个既表示“不存在”又有函数体是不可能的。
或者,在那个概念上,有两个初始化程序。
现在,这就是我的心灵感应、谷歌搜索和软推理所能达到的程度。我推测没有人足够感兴趣去向委员会提出解除这个纯语法限制的建议,并跟进所有相关工作。因此,它仍然保持原样。

17
哇...到目前为止有21个投票,除非我漏掉了什么,你似乎没有回答问题(实际上是为什么行外定义是可能的,但行内不是)。 - Tony Delroy
7
@Tony:很抱歉你没有理解我的回答的相关性。我已经尽力了,但似乎对你没有用。我不知道该如何更清楚地表达。可能是我们之间存在沟通障碍。 - Cheers and hth. - Alf
5
经过8个小时的沉思,我发现在“=0”的狭窄背景下,你所说的是连贯的。我的困惑源于你没有将其与超出范畴的定义相关联或探讨为什么上述思维/逻辑/观点可能不会导致对一个体定义的禁止,或者超出范畴的定义实现了使用,从而改变了关于原地定义的思考方式。这种不一致性是问题的核心所在。如果我们之间仍然存在沟通隔阂,那么我想我们都已经尽力了 - 没有任何伤害 :-)。 - Tony Delroy
如果他把=0移动到virtual后面,那么它就能工作了。所以 virtual =0 void func() { // code }; 把=0放在你可能想要的地方(即紧挨着virtual),尽管语法看起来有点奇怪。 - CashCow
5
就像Tony所说的,你似乎回答了这个问题:“纯虚函数可能没有_任何_定义。为什么?”而不是这个问题:“纯虚函数可能没有内联定义(即使它们可能有一个外部定义)。为什么?” - Jon
显示剩余2条评论

8
你不应该对标准化委员会有如此高的信任度。并非所有事情都有深刻的解释。有些事情之所以是这样,只是因为一开始没有人想到其他可能性,而后来也没有人认为改变它很重要(我认为在这里就是这种情况);对于足够古老的事物,甚至可能是第一个实现的产物。有些事情是演化的结果——曾经有深刻的原因,但原因已被消除,最初的决定没有再次考虑(这也可能是这种情况,在这种情况下,初始决定是因为禁止任何纯函数定义)。有些事情是不同观点之间的协商结果,结果缺乏连贯性,但这种缺乏被认为是达成共识所必需的。

1
根据D&E的说法,纯虚函数是在Stroustrup与AT&T内部C ++用户讨论后添加到C ++中的,仅仅在cfront 2.0发布前几周添加,该版本于1989年6月发布。同一来源称,ANSI X3J16的第一次会议(后来加入ISO WG21形成我们现在所知道的标准委员会)半年后于1989年12月举行。(我曾经认为有一个不允许抽象函数进行内联定义的原因,但我在D&E中找不到它。)因此,这并非由标准委员会决定。 - sbi
1
@AProgrammer:谢谢你的猜测。但我还是要等一个以“So, yeah, I called Bjarne today, and he said ...”开头的答案 :) - Armen Tsirunyan
11
是的,我今天给Bjarne打电话了,但他没有接听。;-) - David Schmitt
@Armen 我不认为这是需要打扰 Bjarne 的问题。干杯! - Cheers and hth. - Alf
2
@David:在过去的15年里,我也许给他发了六封关于C ++的电子邮件。他总是及时回复,非常赞!我猜阿尔曼这样的问题可能每天会有数十个出现在他的收件箱中,所以可能得不到答复。但我不会完全否决这个想法。 - sbi

7

很好的猜测......考虑到情况:

  • 在类内声明函数为内联函数并提供显式的内联函数体是合法的,因此在类内声明函数的唯一实际影响明显没有任何反对意见。
  • 我没有看到任何潜在的语法歧义或冲突,因此没有逻辑上的理由排除在现场定义函数。

我的猜测是:纯虚函数的函数体的用法是在 = 0 | { ... } 语法被制定之后才意识到的,而该语法并未得到修订。值得注意的是,有很多关于语言更改/增强的建议 - 包括使这种事情更加逻辑和一致的建议 - 但是真正被某个人采纳并写成正式建议的数量要少得多,而委员会有时间考虑的建议更少,并且相信编译器供应商将准备好实现它们的建议更少。这些事情需要一个倡导者,也许你是第一个发现其中问题的人。要了解这个过程的感觉,请查看http://www2.research.att.com/~bs/evol-issues.html


2

欢迎提出好的猜测。

我认为在声明中的= 0是基于对实现的考虑。最有可能的定义是,你会在RTTI的类信息的vtbl中得到一个NULL条目,这个位置是存储类的成员函数地址的地方。

但实际上,在您的*.cpp文件中放置函数的定义时,您将向链接器的对象文件引入了一个名称:在*.o文件中找到特定函数的地址。

基本的链接器不再需要了解C++。即使您将其声明为= 0,它也可以进行链接。

我记得您所描述的是可能发生的行为,尽管我忘记了具体细节 :-)...


0

暂且不谈析构函数,纯虚函数的实现是一件奇怪的事情,因为它们从未以自然的方式被调用。也就是说,如果您有一个指向基类的指针或引用,底层对象将始终是某个派生类,该派生类覆盖了该函数,并且该函数将始终被调用。

实际上调用实现的唯一方法是使用派生类重载中的Base::func()语法。

在某些方面,这使得它成为更好的内联目标,因为在编译器想要调用它的时候,总是清楚哪个重载正在被调用。

此外,如果禁止纯虚函数的实现,那么显然会有另一种解决方法,即在基类中使用其他(可能是受保护的)非虚函数,您可以从派生函数中以常规方式调用它。当然,范围会更广泛,您可以从任何函数中调用它。

(顺便说一下,我假设只能从Derived::f()而不是Derived::anyOtherFunc()使用Base::f()语法进行调用。我的假设正确吗?)

纯虚析构函数在某种意义上是一个不同的故事。它被用作一种技术,仅仅是为了防止在没有任何其他纯虚函数的情况下创建派生类的实例。

关于“为什么”不允许使用纯虚析构函数的实际问题的答案只是因为标准委员会这样规定了,但我的答案可以解释我们试图实现的目标。


2
关于不能从Derived::anyOtherFunc()调用它的假设是不正确的。 - sp2danny

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