【问题标题】:C++: Do virtual function calls with a pointer to the derived class still have a vlookupC ++:使用指向派生类的指针进行虚函数调用仍然有一个vlookup
【发布时间】:2014-06-24 10:15:02
【问题描述】:

只是想知道,如果我有一个指向最派生类的指针,并在其上调用最派生类定义的虚函数,这是否仍会导致在虚拟表中查找?

毕竟在编译的时候,编译器就知道这个类是派生最多的,它知道它定义了虚函数,没有歧义,所以就应该把它当成非虚函数吧?

还是我错过了什么?

我问的原因是我正在编写一个模板,稍后我想从中派生来合并代码,并且不同的功能将在派生类中实现。

没有必要在模板中将这些函数定义为虚拟函数,但如果该虚拟调用稍后会被忽略,我仍会考虑这样做,纯粹是为了稍后向实现者可视化哪些函数仍需要编写。

【问题讨论】:

  • 我不会将它们标记为virtual。添加评论或类似内容:/*TEMPLATE-VIRTUAL*/ void foo()。您也可以使用 nul 宏,但我更喜欢注释。
  • 编译器如何在编译时知道一个类是否是最派生类?进一步的派生类在当前编译单元中可能不可见,如果你正在编译一个库,你无法知道用户会进一步派生哪些类(除非你使用 C++11 的final)。

标签: c++ templates inheritance virtual


【解决方案1】:

免责声明:编译器优化

这个答案是关于编译器优化技术的。您的编译器可能支持也可能不支持这些。即使它支持您尝试利用的技术,它们也可能无法在您选择的优化级别上使用。

您的里程可能会有所不同。

最多派生类

编译器确实可以去虚拟化调用如果它知道这确实是最派生的类。讨论如何实现这一点here。一个例子:

struct Base { virtual void call_me_virtual() = nullptr; };
struct Derived final : Base { void call_me_virtual() override { } };

void dosomething(Derived* d) {
    d->call_me_virtual();
}

有趣的是,如果不这样做,您总是可以在另一个翻译单元中让其他人从您的类中派生出,因此编译器不会知道您当前的“更多派生”类翻译单元。

另一种确保类必须是最多派生的方法是将其放入匿名命名空间:

struct Base { virtual void call_me_virtual() = nullptr; };

namespace {
    struct Derived : Base { void call_me_virtual() override { } };

    void dosomething(Derived* d) {
        d->call_me_virtual();
    }
}

这是可行的,因为Derived 在当前翻译单元之外是未知的,这意味着任何更多派生类必须在当前翻译单元内。然而,这意味着 dosomething 不能(正确地)从当前编译单元外部调用,这也是我给它内部链接的原因。

这个规则的一个例外是编译器能够证明对象来自他们的视图,例如。如果Derived 是当前翻译单元中派生最多的类型,并且对象必须始终来自当前翻译单元,则它不能来自任何其他派生类型。通过利用整个程序优化可以扩大这种分析的范围。

已知类型

更常见的情况是编译器确切地知道对象是什么类型,例如在这两种情况下:

Derived d;
d.call_me_virtual();

Base* b = new Derived;
b->call_me_virtual();

编译器可能能够推断出d.call_me_virtualb->call_me_virtual 都将始终解析为Derived::call_me_virtual,从而在该实例中对调用进行去虚拟化(甚至内联)。

在第一种情况下,这需要知道Derived 类型的对象与Derived& 类型非常相似的对象只能是Derived 对象,而不是从它派生的对象。

第二种情况需要静态类型分析,这是由现代优化编译器完成的。通过看到b总是使用指向Derived 对象的指针进行初始化,编译器能够证明b->call_me_virtual 将调用哪个函数。

最终确定的成员函数

当你完成单个成员函数时会发生类似的情况:

struct Base { virtual void call_me_virtual() = nullptr; };
struct Derived : Base { void call_me_virtual() override final { } };

void dosomething(Derived* d) {
    d->call_me_virtual();
}

虽然d 可能指向派生自Derived 的内容,但call_me_virtual 方法可能不再更改,因此编译器知道Derived::call_me_virtual 将始终被调用,因此可以取消虚拟化此调用。

【讨论】:

  • 只是为了说明,请参阅GCC 4.9 中的去虚拟化改进。
  • 一些编译器可以确定一个类是最派生的,即使它没有被声明为final。这通常需要跨模块优化,但如果有问题的类是在未命名的命名空间中定义的,编译器就会知道它在翻译单元之外是不可访问的,无论是派生还是其他。
  • @JamesKanze 我在答案中添加了这些可能性
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-07-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多