头文件中定义的非内联虚函数

4
一定义规则指出:
在整个程序中,一个对象或非内联函数不能有多个定义。(来自维基百科)
嗯,我知道如果成员函数在头文件中定义,它会被隐式地内联,并且符合ODR。
但是虚函数呢?我们知道如果一个虚函数以多态方式调用,它就不能被内联。如果这样的虚函数在头文件中被定义,那么是否会违反ODR?
例如:
//derived.hpp
#include <iostream>
class Base {
public:
  virtual ~Base() {}
  virtual void vfunc() {
    std::cout << "Base::vfunc()\n";
  }
};

class Derived : public Base {
public:
  virtual ~Derived() {}

  virtual void vfunc() {
    std::cout << "Derived::vfunc()\n";
  }
};

//foo.cpp

#include "derived.hpp"
void func() {
  Base* ptr = new Derived();
  ptr->vfunc(); //polymorphic call, can't be inlined
  delete ptr;

  ptr = new Base();
  ptr->vfunc();
  delete ptr;
}
//main.cpp

#include "derived.hpp"

int main() {
  Base* ptr = new Derived();
  ptr->vfunc(); //polymorphic call, can't be inlined
  delete ptr;

  ptr = new Base();
  ptr->vfunc();
  delete ptr;
  return 0;
}

我很好奇:

在foo.cpp和main.cpp中,vfunc(和dtor)以多态方式调用(不是内联),这意味着它在整个程序中被定义了两次,所以它违反了ODR,是吗?它该如何编译(链接)?

我刚刚看到:

多余的定义

在某些情况下,类型或模板可能有多个定义。由多个头文件和源文件组成的程序通常会有多个类型定义,但每个翻译单位只有一个定义。如果程序包含一个类型的多个定义,则每个定义必须等效(也取自维基百科)。

什么是“某些情况”?上述情况是否属于此类情况之一?

2个回答

3
和在foo.cpp和main.cpp中以多态方式被调用(不是内联的),这意味着它们在整个程序中被定义了两次。 首先: 调用并不意味着定义。因此,函数调用不能告诉你ODR是否被违反。 所以它违反了ODR,对吗?那它怎么编译(链接)呢? 它可以编译,因为类定义内部定义的成员函数隐式地被声明为内联,因此不会违反ODR。这适用于定义以及,所以你做得很好。 注意:函数“声明”内联(显式或隐式)和实际内联的函数之间有区别。编译器决定内联一个函数的一次可能会受到inline关键字的影响,但这只是一个提示。现代优化器比任何人都能更好地预测何时内联是一个好选择,何时不内联,因此编译器可以在适当的时候忽略该提示,并内联未被声明为内联的函数。因此,在现代编译器中,“inline”只是遵守未被隐式声明为内联的函数的ODR的手段。 更新:因此,在您的情况下,该函数隐式地被“声明”为内联,该定义包含在两个翻译单元(即两个.cpp文件)中,编译器不会内联该函数。在这种情况下,链接器将看到该函数的符号两次,但由于内联声明,它不会对多个符号进行投诉。

我的意思是:如果一个虚函数被多态地调用,编译器将无法将其内联,因为它不能在编译时解析。 - Frahm
@Frahm 我刚刚编辑了答案 - 请看我的关于 inline 和内联的注释 :-) - Arne Mertz
但至少有一些情况下,虚函数无法进行内联,这些情况下,应该编译吗? - Frahm
@Frahm:是的。ODR完全关乎内联声明。函数是否真正被编译器内联并不影响ODR,以及程序是否有效(即可链接)。顺便说一下:ODR关乎链接,而非编译。我还进一步更新了我的回答。 - Arne Mertz

1
通过在类声明内定义函数,它就是一个内联声明的函数。编译器可能无法将其内联(除非在某些情况下),但这并不改变内联声明的事实。同样可以肯定的是,在每种情况下,您的定义都是相同的(除非您使用宏来更改函数内容,例如在两种情况下未定义相同的cout等)。
另一方面,如果我们在头文件中有以下内容:
class Base {
public:
  virtual ~Base() {}
  virtual void vfunc();
};

class Derived : public Base {
public:
  virtual ~Derived() {}

  virtual void vfunc();
};



void Base::vfunc()
{
  {
    std::cout << "Base::vfunc()\n";
  }
}

void Derived::vfunc()
{
  {
    std::cout << "Derived::vfunc()\n";
  }
}

现在你正在违反ODR,因为Derived::vfunc()没有声明为内联函数,并且由于被多次包含,它被定义了多次(尽管是完全相同的定义)。

所以它没有被内联,并且包含在两个cpp文件中,这意味着被定义了两次? - Frahm
@Frahm:如果函数被声明为内联的,那么两个定义是可以的。 - Arne Mertz
由于“在类定义中定义函数会自动使其成为内联函数”,即使编译器必须将其“离线”使用,这样做也是可以的。请参见编辑部分以了解“如何出错”。 - Mats Petersson

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