内联虚拟功能真的是胡扯吗?


172

当我收到一条代码审查评论时说了虚函数不必是内联的,这是我遇到的问题。

我认为内联虚拟函数可以在直接在对象上调用函数的情况下派上用场。但是我想到的反驳是-为什么要定义虚拟然后使用对象来调用方法?

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

我用于分析的代码段:

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;
}

1
考虑考虑使用所需的任何开关来编译示例,以获取汇编程序列表,然后向代码审阅者显示,实际上,编译器可以内联虚拟函数。
Thomas L Holaday

1
上面的内容通常不会被内联,因为您是在基类的帮助下调用虚函数。尽管这仅取决于编译器的智能程度。如果能够指出pTemp->myVirtualFunction()可以解决为非虚拟调用,则可以内联该调用。g ++ 3.4.2内联了此引用的调用:TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();您的代码不是。
doc 2010年

1
gcc实际上所做的一件事是将vtable条目与特定符号进行比较,然后在匹配的情况下在循环中使用内联变体。如果内联函数为空并且在这种情况下可以消除循环,则这特别有用。
西蒙·里希特

1
@doc现代编译器尽力在编译时确定指针的可能值。仅使用指针不足以防止在任何重要的优化级别进行内联。GCC甚至在优化为零时进行简化!
curiousguy18年

Answers:


153

有时可以内联虚拟函数。摘自出色的C ++常见问题解答

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


7
的确如此,但值得记住的是,即使调用可以在编译时解析并可以内联,编译器也可以忽略内联说明符。
sharptooth

6
我认为可能发生内联的另一种情况是,当您调用此方法时,例如this-> Temp :: myVirtualFunction()-这样的调用会跳过虚拟表的解析,并且函数应该内联而不会出现问题-为什么以及是否d想要做的是另一个主题:)
RnR

5
@RnR。不必使用'this->',只需使用限定名称即可。这种行为发生在析构函数,构造函数上,并且通常发生在赋值运算符上(请参见我的回答)。
理查德·科登

2
sharptooth-是的,但对于所有内联函数,不仅是虚拟内联函数,这都是正确的。
科伦

2
void f(const Base&lhs,const Base&rhs){} ------在函数的实现中,直到运行时,您才知道lhs和rhs指向什么。
Baiyan Huang

72

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.
}

无法在VS 2017中进行内联
。– Yola

1
我不认为这种方式有效。通过A类型的指针/引用对foo()的调用永远不能内联。调用b.foo()应该允许内联。除非您建议编译器已经知道这是B型,否则是因为它知道上一行。但这不是典型的用法。
杰弗里·福斯特

例如,在此处比较生成的bar和bas代码:godbolt.org/g/xy3rNh
Jeffrey Faust

@JeffreyFaust没有理由不应该传播信息,是吗?而且icc似乎这样做,根据该链接。
阿列克谢·罗曼诺夫

@AlexeyRomanov编译器可以自由进行超出标准的优化,当然可以!对于上述简单情况,编译器可以知道类型并进行优化。事情很少这么简单,并且在编译时确定多态变量的实际类型并不常见。我认为OP关心的是“一般”而不是针对这些特殊情况。
杰弗里·福斯特

37

有一类虚拟函数使内联仍然有意义。考虑以下情况:

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'的调用将执行虚拟调用,以调用正确的派生类析构函数,该调用未内联。但是,由于每个析构函数都调用其父析构函数(在这种情况下为空),因此编译器可以内联这些调用,因为它们实际上并未调用基类函数。

对于基类构造函数或其中派生实现也称为基类实现的任何函数集,存在相同的原理。


23
应该知道,尽管空括号并不总是意味着析构函数不执行任何操作。析构函数会默认破坏类中的每个成员对象,因此,如果基类中有一些向量,那么在这些空括号中可能会做很多工作!
菲利普(Philip)

14

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

确实,这与标准不符,但是它确实发生了,因此请考虑将至少一个虚拟函数放在标头中(如果只有虚拟析构函数的话),以便编译器可以在该位置为类发出vtable。我知道某些版本的会发生这种情况gcc

就像有人提到的那样,内联虚拟函数有时可能是一个好处,但是当然,最常见的情况是您在知道对象的动态类型的情况下会使用它,因为这首先就是全部原因virtual

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


11

好吧,实际上虚拟函数总是可以内联的,只要它们静态链接在一起即可:假设我们有一个抽象类,Base 其中包含一个虚函数F和派生类Derived1以及Derived2

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-的函数,该函数返回可以在中使用的值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
}

他们有这样做的编译器吗?还是只是猜测?很抱歉,如果我过于怀疑,但是上面描述中的语气听起来像是“它们完全可以做到!”,这与“某些编译器可以做到这一点”不同。
亚历克斯·梅博格

是的,Graal进行了多态内联(也适用于通过
Sulong的


3

内联实际上什么也没做-这只是一个提示。如果编译器看到了实现并喜欢这个主意,则它可能会忽略它,或者内联一个没有联的调用事件。如果代码清晰受到威胁,则应删除内联


2
对于仅在单个TU上运行的编译器,它们只能内联具有其定义的隐式函数。如果将函数内联,则只能在多个TU中定义一个函数。“内联”不仅仅是一个提示,对于g ++ / makefile构建,它可以显着提高性能。
理查德·科登,2009年

3

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


1

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


1

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

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


1
当您从相同或派生类中调用基类方法时,该调用是明确且非虚拟的
sharptooth 2009年

1
@sharptooth:但是那将是一种非虚拟的内联方法。编译器可以内联您不要求的函数,并且它可能更清楚何时内联。让它决定。
大卫·罗德里格斯(DavidRodríguez)-dribeas,2009年

1
@dribeas:是的,这正是我在说的。我只反对在运行时解析虚拟函数的说法-仅当虚拟完成调用时才是这样,而不是针对确切的类,这是正确的。
sharptooth

我相信那是胡扯。任何函数总是可以内联的,无论它有多大或是否虚拟。这取决于编译器的编写方式。如果您不同意,那么我希望您的编译器也无法生成非内联代码。也就是说:编译器可以包含在运行时测试其在编译时无法解析的条件的代码。就像现代的编译器可以在编译时解析常量值/减少数字表达式一样。如果未内联函数/方法,则并不意味着无法内联。

1

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

其余时间“内联虚拟”是胡说八道,确实有些编译器不会编译该代码。


哪个版本的g ++无法编译内联虚拟函数?
Thomas L Holaday

嗯 我现在在这里看到的4.1.1似乎很高兴。我首先在使用4.0.x的此代码库中遇到问题。猜猜我的信息已经过时,已编辑。
moonshadow

0

制作虚函数,然后在对象而不是引用或指针上调用虚函数确实是有意义的。斯科特·迈耶(Scott Meyer)在他的《有效的c ++》一书中建议不要重新定义继承的非虚函数。这是有道理的,因为当您使用非虚函数创建一个类并在派生类中重新定义该函数时,您可能会确定自己会正确使用它,但不能确保其他人会正确使用它。另外,您以后可能会错误地自行使用。因此,如果您在基类中创建一个函数并希望其可重定义,则应将其虚拟化。如果有意义的是创建虚函数并在对象上调用它们,则内联它们也很有意义。


0

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

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

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

有趣的是,我不断从测试项目中收到链接器错误。即使我通过复制粘贴(而不是通过包含)添加了缺少功能的定义!太奇怪了。另一个项目已经构建,除了标记项目引用之外,两者之间没有任何联系,因此有一个构建顺序来确保始终都构建这两个项目。

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

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

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

PS(Linux):我希望在gcc或clang中不会像我通常用来做这类事情的情况一样。我不确定此问题的来源...我更喜欢在Linux上或至少在某些gcc上使用c ++,但有时项目的需求有所不同。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.