如果虚函数不是纯虚函数,那么它就被称为虚成员函数?

8

C++03 3.2.2 ...如果一个对象或非重载函数的名称出现在可能被评估的表达式中,则使用该对象或函数。如果虚成员函数不是纯函数,则使用它...

然后在3.2.3中,我们有:每个程序必须包含在该程序中使用的每个非内联函数或对象的正好一个定义; 不需要诊断。定义可以在程序中明确出现,可以在标准或用户定义库中找到,或者(在适当的情况下)它是隐式定义的(见12.1、12.4和12.8)。 每个内联函数都必须在使用它的每个翻译单元中定义。

根据我的阅读理解:不使用纯虚函数。ODR仅适用于使用的函数。这是否意味着以下操作是合法的?我猜答案是否定的,但我无法理解为什么。

//x.h
struct A
{
   virtual void f() = 0;
};

//y.cpp
#include "x.h"
void A::f()
{
}

//z.cpp
#include "x.h"
#include <iostream>
void A::f()
{
   std::cout << "Hello" << std::endl;
}

//main.cpp
#include "x.h"
struct B:A
{
   virtual void f()
   {
      A::f();
   }
};

int main()
{
   A* p = new B;
   p->f();
}

这个能编译/链接吗?我感觉会有一个链接错误,因为有一个重复的结构体A实现。 - Stephane Rolland
1
@Stephane Rolland:请再次阅读标准:“每个程序必须包含在该程序中使用的每个非内联函数的确切定义[...]”和“如果虚成员函数不是纯虚函数,则被视为使用”。这意味着,如果虚成员函数是纯虚函数,则可以解释标准为它未被使用,这将允许任意多个定义。 - Philipp
1
GCC 链接器确实会给出错误(即使没有 main.cpp),但这一点在标准中并不清楚,甚至是错误的,这取决于 if 的含义。 - Philipp
我不理解你的标准。是哪些参数让你说“任意多个定义”? - Stephane Rolland
2
@Stephanie:我认为我们都同意,这个例子违反了标准的意图,它引起问题是很正常的。讨论的重点是标准措辞是否意外地忘记了涵盖这种情况。 - aschepler
显示剩余13条评论
5个回答

11

这两个条款并不是互相排斥的。虚函数如果不是纯的,就会被使用,但这并不意味着反过来也成立。如果虚函数是纯的,那并不意味着它一定不会被使用。它仍然可以被使用,比如在你的例子中:

A::f();

当其名称出现在“可能会被评估的表达式”中时。


1
除非将3.2p2解读为符号使用了任何一个单独的要求,否则它没有意义。例如,分配函数被new表达式使用。但如果使用operator new进行显式调用,它肯定也会被使用。 - aschepler
2
@Armen Tsirunyan:请再仔细阅读一下条款。纯虚函数没有例外;例外是针对非纯虚函数的,无论它们是否以任何其他方式被使用,都会自动使用。 - CB Bailey
1
@Armen Tsirunyan:是的。有一个实际的原因:在许多C++实现中,这样一个函数的地址需要放入任何从未覆盖该函数的类派生的对象的虚表中。这样的类可能仅在稍后编译的另一个翻译单元中定义,因此如果这样的函数在所有情况下都被视为“已使用”,那么对于实现来说是最简单的,因此实现可以依赖于能够在链接时找到该函数。纯虚函数必须在任何可实例化的类中被覆盖,因此这对它们不是一个问题。 - CB Bailey
1
@Armen关于p->f()的观点是正确的。这实际上会使用纯虚函数,但这当然不是预期的结果。然而,已经有一个问题报告在http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1174上了。 - Johannes Schaub - litb
@litb:我非常有意地避免对x->f()是否使用纯虚函数做出任何判断;我不需要回答原始问题。 ;) - CB Bailey
显示剩余3条评论

3

这段代码违反了ODR。A::f被多次定义,因此它具有未定义行为。

根据$3.2/5的规定,只有以下情况允许跨翻译单元存在多个定义:

在程序中,可以有一个类类型(第9条),枚举类型(第7.2条),带有外部链接的内联函数(第7.1.2条),类模板(第14条),非静态函数模板(第14.5.5条),类模板的静态数据成员(第14.5.1.3条),类模板的成员函数(第14.5.1.1条)或某些模板参数未指定的模板特化(第14.7、14.5.4条)的多个定义,前提是每个定义出现在不同的翻译单元中,并且满足以下要求。


1

正如@Charles Bailey所指出的那样,即使它是纯虚函数,您的A::f实际上仍在使用。但这已经不是重点了。

并不准确的是,只有未使用的函数不适用于“单一定义规则”。我们有:

3.2p1 任何变量、函数、类类型、枚举类型或模板的翻译单元都不得包含多个定义。

3.2p3 每个程序必须包含每个在该程序中使用的非内联函数或对象的一个定义;无需诊断。

综合这些要求似乎意味着一个被使用的函数必须具有唯一的定义,而一个未使用的函数(包括从未显式调用的纯虚函数)可以没有定义或者只有一个定义。在任一情况下,对于非内联函数的多个定义会使程序不符合规范。

至少,我非常确定这就是意图。但是,由于文字表述上的漏洞,不同翻译单元中相同未使用函数的多个不同定义是否不符合规范却没有明确说明。

// x.cpp
void f() {}
void g() {}

// y.cpp
#include <iostream>
void f() {
  std::cout << "Huh" << std::endl;
}
void h() {}

// z.cpp
void g();
void h();
int main() { 
  g();
  h();
  return 0;
}

不用等,3.2p1确实提到了“翻译单元”。我认为实际上可能存在措辞漏洞。 - aschepler
3.2/1 不是短语漏洞。它适用于内联函数、类和枚举,这些可以在每个翻译单元中定义一次,而非内联函数和对象则不行。 - CB Bailey
@Charles:同意。但是哪个需求说明了我的样例程序不正确?第一段不适用,因为定义在不同的翻译单元中。P2没有说f()被使用。P3不适用,因为它没有被使用。P4是关于类定义的。P5是关于包括内联函数在内的事物,这些函数可以被多次定义。 - aschepler
我认为它应该是3.2/5,这个评级准确描述了程序中哪些实体可能具有多个定义以及多个定义的限制。我承认,如果实体不在列表中,那么它可能没有多于一个定义的含义只能是零或一个定义,这只是通过暗示得出的。 - CB Bailey
我之前在Usenet上发布了这个问题,后来发现这是一个缺陷。但我没有将其作为问题报告提交给委员会。你可以选择这样做。 - Johannes Schaub - litb

1
这个话题有关但不是主题:从引用中看来,标准确实存在一个漏洞:它应该说明使用纯虚析构函数,并且必须定义;至少如果存在任何被销毁的派生类对象或者定义了这样的析构函数,因为派生类析构函数必须隐式调用基类析构函数,它会使用qualified::id语法进行调用。这种析构函数的定义通常是微不足道的,但不能省略也不能生成。

12.4p7:“析构函数可以声明为virtual(10.3)或纯虚virtual(10.4);如果程序中创建了该类或任何派生类的任何对象,则必须定义析构函数。”(我的注释:“对象”包括成员子对象。) - aschepler

0

[class.abstract]: "只有在使用限定符标识语法(5.1)调用纯虚函数时,才需要定义它。"

B::f调用了您的A::f,因此必须有一个单一的定义A::f


我同意根据您的引用,A::f必须有一个定义。但我没有看到哪个条款要求有一个唯一的定义。 - Armen Tsirunyan
@Armen Tsirunyan:请看我的回复以获得澄清。 - Chubsdad

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