188

当我收到一条代码审查评论说虚函数不需要内联时,我得到了这个问题。

我认为内联虚函数可以在直接在对象上调用函数的场景中派上用场。但我想到的相反论点是——为什么要定义虚拟然后使用对象来调用方法?

最好不要使用内联虚函数,因为它们几乎从未扩展过?

我用于分析的代码片段:

class Temp
{
public:

    virtual ~Temp()
    {
    }
    virtual void myVirtualFunction() const
    {
        cout<<"Temp::myVirtualFunction"<<endl;
    }

};

class TempDerived : public Temp
{
public:

    void myVirtualFunction() const
    {
        cout<<"TempDerived::myVirtualFunction"<<endl;
    }

};

int main(void) 
{
    TempDerived aDerivedObj;
    //Compiler thinks it's safe to expand the virtual functions
    aDerivedObj.myVirtualFunction();

    //type of object Temp points to is always known;
    //does compiler still expand virtual functions?
    //I doubt compiler would be this much intelligent!
    Temp* pTemp = &aDerivedObj;
    pTemp->myVirtualFunction();

    return 0;
}
4

13 回答 13

162

有时可以内联虚函数。优秀C++ 常见问题的摘录:

“内联虚调用唯一可以内联的时间是当编译器知道作为虚函数调用目标的对象的“确切类”时。只有当编译器有一个实际对象而不是指针或指针时,才会发生这种情况。对对象的引用。即,使用本地对象、全局/静态对象或组合内的完全包含对象。

于 2009-04-09T11:23:18.557 回答
80

C++11 增加了final. 这改变了公认的答案:不再需要知道对象的确切类,只要知道对象至少具有函数声明为 final 的类类型就足够了:

class A { 
  virtual void foo();
};
class B : public A {
  inline virtual void foo() final { } 
};
class C : public B
{
};

void bar(B const& b) {
  A const& a = b; // Allowed, every B is an A.
  a.foo(); // Call to B::foo() can be inlined, even if b is actually a class C.
}
于 2013-11-13T12:45:09.810 回答
40

有一类虚函数仍然可以将它们内联。考虑以下情况:

class Base {
public:
  inline virtual ~Base () { }
};

class Derived1 : public Base {
  inline virtual ~Derived1 () { } // Implicitly calls Base::~Base ();
};

class Derived2 : public Derived1 {
  inline virtual ~Derived2 () { } // Implicitly calls Derived1::~Derived1 ();
};

void foo (Base * base) {
  delete base;             // Virtual call
}

删除'base'的调用,将执行一个虚拟调用来调用正确的派生类析构函数,这个调用不是内联的。然而,因为每个析构函数都调用它的父析构函数(在这些情况下是空的),编译器可以内联这些调用,因为它们实际上并不调用基类函数。

对于基类构造函数或派生实现也调用基类实现的任何函数集,存在相同的原则。

于 2009-04-09T12:53:52.317 回答
14

如果根本不存在非内联函数(然后在一个实现文件而不是头文件中定义),我已经看到编译器不会发出任何 v-table。他们会抛出类似missing vtable-for-class-A或类似的错误,你会像我一样感到困惑。

事实上,这不符合标准,但它发生了,所以考虑将至少一个虚函数放在头文件中(如果只有虚拟析构函数),以便编译器可以在那个地方为类发出一个 vtable。我知道它发生在某些版本的gcc.

正如有人提到的,内联虚函数有时会带来好处,但当然,当您知道对象的动态类型时,您通常会使用它,因为这首先是全部原因virtual

然而,编译器不能完全忽略inline. 除了加速函数调用之外,它还有其他语义。类内定义的隐式内联是允许您将定义放入标题的机制:只有inline函数可以在整个程序中多次定义而不会违反任何规则。最后,它的行为就像您在整个程序中只定义一次一样,即使您将标题多次包含到链接在一起的不同文件中。

于 2009-04-09T14:44:11.647 回答
13

好吧,实际上虚函数总是可以内联的,只要它们静态链接在一起:假设我们有一个Base 带有虚函数F和派生类的抽象类Derived1Derived2

class Base {
  virtual void F() = 0;
};

class Derived1 : public Base {
  virtual void F();
};

class Derived2 : public Base {
  virtual void F();
};

一个假设的呼叫b->F();b类型Base*为 )显然是虚拟的。但是你(或编译器......)可以像这样重写它(假设typeof是一个类似typeid- 的函数,它返回一个可以在 a 中使用的值switch

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // static, inlineable call
  case Derived2: b->Derived2::F(); break; // static, inlineable call
  case Base:     assert(!"pure virtual function call!");
  default:       b->F(); break; // virtual call (dyn-loaded code)
}

虽然我们仍然需要 RTTI typeof,但调用可以有效地内联,基本上,将 vtable 嵌入指令流中并专门针对所有涉及的类调用。这也可以通过只专门化几个类来概括(比如,只是Derived1):

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // hot path
  default:       b->F(); break; // default virtual call, cold path
}
于 2011-11-18T20:46:52.173 回答
4

将虚方法标记为内联,有助于在以下两种情况下进一步优化虚函数:

于 2013-09-15T04:41:08.387 回答
3

inline真的没有做任何事情 - 这是一个提示。编译器可能会忽略它,或者如果它看到实现并喜欢这个想法,它可能会在没有内联的情况下内联调用事件。如果代码清晰度受到威胁,则应删除内联。

于 2009-04-09T11:07:43.323 回答
3

内联声明的虚函数在通过对象调用时被内联,而在通过指针或引用调用时被忽略。

于 2011-08-01T02:31:17.727 回答
1

只有在编译时可以明确解析调用时,编译器才能内联函数。

然而,虚函数是在运行时解析的,因此编译器无法内联调用,因为在编译类型时无法确定动态类型(以及因此要调用的函数实现)。

于 2009-04-09T11:06:33.200 回答
1

使用现代编译器,它不会对它们造成任何伤害。一些古老的编译器/链接器组合可能已经创建了多个 vtable,但我认为这不再是一个问题。

于 2009-04-09T11:08:17.750 回答
1

在函数调用明确且函数适合内联的情况下,编译器足够聪明,无论如何都可以内联代码。

其余时间“内联虚拟”是无稽之谈,确实有些编译器不会编译该代码。

于 2009-04-09T11:09:58.387 回答
0

创建虚函数然后在对象而不是引用或指针上调用它们确实有意义。Scott Meyer 在他的《有效的 c++》一书中建议永远不要重新定义继承的非虚拟函数。这是有道理的,因为当你创建一个带有非虚函数的类并在派生类中重新定义该函数时,你可能确定自己会正确使用它,但你无法确定其他人会正确使用它。此外,您以后可能会错误地使用它。所以,如果你在基类中创建一个函数并且你希望它是可重定义的,你应该让它成为虚拟的。如果创建虚函数并在对象上调用它们是有意义的,那么内联它们也是有意义的。

于 2012-04-25T22:03:29.860 回答
0

实际上,在某些情况下,将“内联”添加到虚拟最终覆盖可能会使您的代码无法编译,因此有时会有所不同(至少在 VS2017s 编译器下)!

实际上,我在 VS2017 中执行了一个虚拟内联最终覆盖函数,添加了 c++17 标准来编译和链接,由于某种原因,当我使用两个项目时它失败了。

我有一个测试项目和一个我正在单元测试的实现 DLL。在测试项目中,我有一个“linker_includes.cpp”文件,其中 #include 来自所需的其他项目的 *.cpp 文件。我知道...我知道我可以设置 msbuild 以使用 DLL 中的目标文件,但请记住,它是 Microsoft 特定的解决方案,而包含 cpp 文件与构建系统无关并且更容易版本一个cpp文件而不是xml文件和项目设置等等......

有趣的是,我不断地从测试项目中得到链接器错误。即使我通过复制粘贴而不是通过包含添加了缺失函数的定义!太奇怪了。另一个项目已经建成,除了标记项目参考之外,两者之间没有任何联系,因此有一个构建顺序来确保始终构建两者......

我认为这是编译器中的某种错误。我不知道它是否存在于 VS2020 附带的编译器中,因为我使用的是旧版本,因为某些 SDK 只能正常工作:-(

我只是想补充一点,不仅将它们标记为内联可能意味着什么,甚至可能使您的代码在某些极少数情况下无法构建!这很奇怪,但很高兴知道。

PS.:我正在处理的代码与计算机图形相关,所以我更喜欢内联,这就是我同时使用 final 和 inline 的原因。我保留了最后的说明符,希望发布版本足够聪明,可以通过内联它来构建 DLL,即使我没有直接暗示......

PS(Linux)。:我希望在 gcc 或 clang 中不会发生同样的事情,因为我经常做这些事情。我不确定这个问题来自哪里......我更喜欢在 Linux 上执行 c++ 或至少使用一些 gcc,但有时项目的需求不同。

于 2020-06-29T18:35:27.937 回答