如何实现虚拟功能和vtable?

问题描述 投票:98回答:12

我们都知道C ++中的虚函数是什么,但它们是如何在深层次实现的?

可以在运行时修改甚至直接访问vtable吗?

vtable是否适用于所有类,或仅适用于至少具有一个虚函数的类?

抽象类对于至少一个条目的函数指针只有一个NULL吗?

有一个虚拟函数会减慢整个班级的速度吗?或者只调用虚拟函数?如果虚拟功能实际被覆盖了,速度是否会受到影响,或者只要它是虚拟的,它就没有效果。

c++ polymorphism virtual-functions vtable
12个回答
114
投票

虚拟功能如何在深层次实施?

来自"Virtual Functions in C++"

只要程序声明了虚函数,就会为该类构造一个v表。 v表包含包含一个或多个虚函数的类的虚函数的地址。包含虚函数的类的对象包含一个指向内存中虚拟表的基址的虚拟指针。每当有虚函数调用时,v表用于解析函数地址。包含一个或多个虚函数的类的对象在内存中对象的最开头包含一个名为vptr的虚拟指针。因此,在这种情况下,对象的大小增加了指针的大小。此vptr包含内存中虚拟表的基址。注意,虚拟表是特定于类的,即,对于类只有一个虚拟表,而不管它包含的虚函数的数量。该虚拟表又包含该类的一个或多个虚函数的基地址。在对象上调用虚函数时,该对象的vptr为内存中的该类提供虚拟表的基址。此表用于解析函数调用,因为它包含该类的所有虚函数的地址。这是在虚函数调用期间解析动态绑定的方式。

可以在运行时修改甚至直接访问vtable吗?

一般来说,我认为答案是“不”。你可以做一些内存修改来找到vtable,但你仍然不知道函数签名是什么样的。如果没有直接访问vtable或在运行时修改它,应该可以使用此功能(语言支持)实现的任何功能。另请注意,C ++语言规范没有指定需要vtable - 但这就是大多数编译器实现虚函数的方式。

vtable是否适用于所有对象,或仅存在至少具有一个虚函数的对象?

我相信这里的答案是“它取决于实现”,因为规范首先不需要vtable。但是,在实践中,我相信如果一个类至少有一个虚函数,所有现代编译器都只创建一个vtable。存在与vtable相关联的空间开销以及与调用虚拟功能与非虚拟功能相关联的时间开销。

抽象类对于至少一个条目的函数指针只有一个NULL吗?

答案是它没有通过语言规范指定,因此它取决于实现。如果未定义(通常不是),则调用纯虚函数会导致未定义的行为(ISO / IEC 14882:2003 10.4-2)。实际上,它确实在vtable中为函数分配了一个槽,但是没有为它分配地址。这使得vtable不完整,这需要派生类来实现该函数并完成vtable。有些实现只是在vtable条目中放置一个NULL指针;其他实现将指针指向一个虚拟方法,该方法执行类似于断言的操作。

请注意,抽象类可以定义纯虚函数的实现,但该函数只能使用qualified-id语法调用(即,在方法名称中完全指定类,类似于从中调用基类方法)派生类)。这样做是为了提供易于使用的默认实现,同时仍然要求派生类提供覆盖。

有一个虚函数会减慢整个类的速度,还是只调用虚函数?

这是我的知识的边缘,所以如果我错了,有人请帮助我!

我相信只有类中虚拟的函数才会遇到与调用虚函数和非虚函数相关的时间性能。这个类的空间开销是两种方式。请注意,如果存在vtable,则每个类只有1个,而不是每个对象一个。

如果虚拟功能实际被覆盖,速度是否会受到影响,或者只要它是虚拟的,它是否会起作用?

我不相信,与调用基本虚函数相比,被覆盖的虚函数的执行时间会减少。但是,与为派生类和基类定义另一个vtable相关联的类还有一个额外的空间开销。

其他资源:

qazxsw poi(通过背驮式机器) qazxsw poi qazxsw poi


1
投票

所有这些答案中没有提到的是在多重继承的情况下,基类都有虚拟方法。继承类有多个指向vmt的指针。结果是这样一个对象的每个实例的大小更大。每个人都知道具有虚方法的类对于vmt有4个字节的额外内容,但是在多重继承的情况下,对于每个基类,其具有虚拟方法的时间为4. 4是指针的大小。


0
投票

Burly的答案在这里是正确的,除了这个问题:

抽象类对于至少一个条目的函数指针只有一个NULL吗?

答案是没有为抽象类创建任何虚拟表。没有必要,因为不能创建这些类的对象!

换句话说,如果我们有:

*

通过pB访问的vtbl指针将是D类的vtbl。这正是多态性的实现方式。也就是说,如何通过pB访问D方法。 B类不需要vtbl。

回应Mike的评论如下......

如果我的描述中的B类有一个虚拟方法foo()没有被D覆盖,而虚拟方法bar()被覆盖,那么D的vtbl将有一个指向B的foo()和它自己的bar()的指针。仍然没有为B创建vtbl。


0
投票

非常可爱的概念证明我提前做了一点(看看遗产的顺序是否重要);让我知道你的C ++实现是否真的拒绝它(我的gcc版本只给出了分配匿名结构的警告,但那是一个bug),我很好奇。

CCPolite.h:

->

CCPolite_constructor.h:

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}

main.c中:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

输出:

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

请注意,因为我从不分配我的假物品,所以不需要做任何破坏;析构函数自动放在动态分配对象范围的末尾,以回收对象文本本身和vtable指针的内存。


28
投票
  • 可以在运行时修改甚至直接访问vtable吗?

不便携,但如果你不介意肮脏的技巧,当然!

警告:不建议儿童,http://www.codersource.net/published/view/325/virtual_functions_in.aspx以下的成年人或Alpha Centauri的小型毛茸茸生物使用此技术。副作用可能包括http://en.wikipedia.org/wiki/Virtual_tablehttp://www.codesourcery.com/public/cxx-abi/abi.html#vtable作为所有后续代码审查的必要批准者的突然出现,或969追溯性添加到所有现有实例]

在我看过的大多数编译器中,vtbl *是对象的前4个字节,而vtbl内容只是一个成员指针数组(通常按照它们被声明的顺序,基类是第一个)。当然还有其他可能的布局,但这是我一般观察到的。

demons which fly out of your nose

现在要拉一些恶作剧......

在运行时更改类:

Yog-Sothoth

替换所有实例的方法(monkeypatching class)

这个有点棘手,因为vtbl本身可能只在只读内存中。

IHuman::PlayPiano()

由于mprotect操作,后者很可能使病毒检查程序和链接唤醒并注意到。在使用NX位的过程中,它可能会失败。


17
投票

有一个虚拟函数会减慢整个班级的速度吗?

或者只调用虚拟函数?如果虚拟功能实际被覆盖了,速度是否会受到影响,或者只要它是虚拟的,它就没有效果。

只要在处理这样一个类的对象时,必须初始化,复制另一个数据项,虚函数就会减慢整个类的速度。对于有六个左右成员的班级,差异应该是可以忽略的。对于只包含一个class A { public: virtual int f1() = 0; }; class B : public A { public: virtual int f1() { return 1; } virtual int f2() { return 2; } }; class C : public A { public: virtual int f1() { return -1; } virtual int f2() { return -2; } }; A *x = new B; A *y = new C; A *z = new C; 成员的类,或者根本没有成员的类,差异可能是值得注意的。

除此之外,重要的是要注意,并非每次调用虚函数都是虚函数调用。如果你有一个已知类型的对象,编译器可以为正常的函数调用发出代码,甚至可以在感觉就好的情况下内联所述函数。只有当您通过可能指向基类的对象或某个派生类的对象的指针或引用进行多态调用时,您才需要vtable间接并根据性能付费。

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

无论函数是否被覆盖,硬件必须采取的步骤基本相同。从对象读取vtable的地址,从适当的槽中检索函数指针,以及由指针调用的函数。就实际绩效而言,分支预测可能会产生一些影响。因此,例如,如果您的大多数对象引用给定虚函数的相同实现,那么即使在检索指针之前,分支预测器也有可能正确地预测要调用的函数。但是哪个函数是常见函数无关紧要:它可能是委托给非重写基本案例的大多数对象,或属于同一子类的大多数对象,因此委托给同一个被覆盖的案例。

他们是如何在深层次实施的?

我喜欢jheriko的想法,使用模拟实现来证明这一点。但是我使用C来实现类似于上面代码的东西,因此更容易看到低级别。

parent class Foo

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

derived class Bar

char

function f performing virtual function call

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

所以你可以看到,vtable只是内存中的一个静态块,主要包含函数指针。多态类的每个对象都将指向与其动态类型对应的vtable。这也使得RTTI和虚函数之间的连接更加清晰:您可以通过查看它所指向的vtable来检查类的类型。以上是以许多方式简化的,例如,多重继承,但一般概念是健全的。

如果typedef struct Foo_t Foo; // forward declaration struct slotsFoo { // list all virtual functions of Foo const void *parentVtable; // (single) inheritance void (*destructor)(Foo*); // virtual destructor Foo::~Foo int (*a)(Foo*); // virtual function Foo::a }; struct Foo_t { // class Foo const struct slotsFoo* vtable; // each instance points to vtable }; void destructFoo(Foo* self) { } // Foo::~Foo int aFoo(Foo* self) { return 1; } // Foo::a() const struct slotsFoo vtableFoo = { // only one constant table 0, // no parent class destructFoo, aFoo }; void constructFoo(Foo* self) { // Foo::Foo() self->vtable = &vtableFoo; // object points to class vtable } void copyConstructFoo(Foo* self, Foo* other) { // Foo::Foo(const Foo&) self->vtable = &vtableFoo; // don't copy from other! } typedef struct Bar_t { // class Bar Foo base; // inherit all members of Foo } Bar; void destructBar(Bar* self) { } // Bar::~Bar int aBar(Bar* self) { return 2; } // Bar::a() const struct slotsFoo vtableBar = { // one more constant table &vtableFoo, // can dynamic_cast to Foo (void(*)(Foo*)) destructBar, // must cast type to avoid errors (int(*)(Foo*)) aBar }; void constructBar(Bar* self) { // Bar::Bar() self->base.vtable = &vtableBar; // point to Bar vtable } 类型并且您使用void f(Foo* arg) { // same functionality as above Foo x; constructFoo(&x); aFoo(&x); Bar y; constructBar(&y); aBar(&y); arg->vtable->a(arg); // virtual function call Foo z; copyConstructFoo(&z, arg); aFoo(&z); destructFoo(&z); destructBar(&y); destructFoo(&x); } ,但实际上是arg类型的对象,那么您仍然可以获得Foo*的正确地址。这是因为arg->vtable始终是对象地址的第一个元素,无论它是在正确类型的表达式中称为Bar还是vtable


2
投票

通常使用VTable,指向函数的指针数组。


2
投票

这个答案已被纳入vtable

  • 抽象类对于至少一个条目的函数指针只有一个NULL吗?

答案是它未指定 - 调用纯虚函数会导致未定义的行为(如果未定义)(通常不是)(ISO / IEC 14882:2003 10.4-2)。有些实现只是在vtable条目中放置一个NULL指针;其他实现将指针指向一个虚拟方法,该方法执行类似于断言的操作。

请注意,抽象类可以定义纯虚函数的实现,但该函数只能使用qualified-id语法调用(即,在方法名称中完全指定类,类似于从中调用基类方法)派生类)。这样做是为了提供易于使用的默认实现,同时仍然要求派生类提供覆盖。


2
投票

您可以使用函数指针作为类的成员和静态函数作为实现,或使用指向成员函数的指针和实现的成员函数,在C ++中重新创建虚函数的功能。这两种方法之间只有符号优势......事实上,虚函数调用本身只是一种符号方便。事实上,继承只是一种符号方便性......它可以在不使用语言特性进行继承的情况下实现。 :)

下面是未经测试的废话,可能是错误的代码,但希望能够证明这个想法。

EG

vtable

2
投票

我会试着让它变得简单:)

我们都知道C ++中的虚函数是什么,但它们是如何在深层次实现的?

这是一个带有指向函数的指针的数组,这些函数是特定虚函数的实现。此数组中的索引表示为类定义的虚函数的特定索引。这包括纯虚函数。

当多态类派生自另一个多态类时,我们可能会遇到以下情况:

  • 派生类不会添加新的虚函数,也不会覆盖任何虚函数。在这种情况下,此类与基类共享vtable。
  • 派生类添加并覆盖虚拟方法。在这种情况下,它获得自己的vtable,其中添加的虚函数的索引从最后一个派生开始。
  • 继承中有多个多态类。在这种情况下,我们在第二个和下一个基数之间有一个索引转换,并且它在派生类中的索引

可以在运行时修改甚至直接访问vtable吗?

不标准的方式 - 没有API可以访问它们。编译器可能有一些扩展或私有API来访问它们,但这可能只是一个扩展。

vtable是否适用于所有类,或仅适用于至少具有一个虚函数的类?

只有那些至少有一个虚函数(甚至是析构函数)或派生至少一个具有vtable的类(“是多态的”)。

抽象类对于至少一个条目的函数指针只有一个NULL吗?

这是一种可能的实施方式,但并未实施。相反,通常有一个函数可以打印类似“纯虚函数调用”和base.vtable。如果您尝试在构造函数或析构函数中调用抽象方法,则可能会发生对此的调用。

有一个虚拟函数会减慢整个班级的速度吗?或者只调用虚拟函数?如果虚拟功能实际被覆盖了,速度是否会受到影响,或者只要它是虚拟的,它就没有效果。

减速仅取决于呼叫是作为直接呼叫还是作为虚拟呼叫解决。没有其他事情重要。 :)

如果通过指针或对象的引用来调用虚函数,那么它将始终实现为虚拟调用 - 因为编译器永远不会知道在运行时将为此指针分配哪种对象,以及它是否为是否重写此方法的类。只有在两种情况下,编译器才能将对虚拟函数的调用解析为直接调用:

  • 如果通过值(变量或返回值的函数的结果)调用方法 - 在这种情况下,编译器不会怀疑对象的实际类是什么,并且可以在编译时“硬解析”它。
  • 如果虚拟方法在类中声明为Community Wiki answer,您可以通过该方法调用它(仅在C ++ 11中)。在这种情况下,编译器知道此方法不能进行任何进一步的覆盖,它只能是此类中的方法。

请注意,虚拟调用只有解除引用两个指针的开销。使用RTTI(尽管只适用于多态类)比调用虚方法慢,如果你找到一个案例来实现同样的两种方式。例如,定义class Foo { protected: void(*)(Foo*) MyFunc; public: Foo() { MyFunc = 0; } void ReplciatedVirtualFunctionCall() { MyFunc(*this); } ... }; class Bar : public Foo { private: static void impl1(Foo* f) { ... } public: Bar() { MyFunc = impl1; } ... }; class Baz : public Foo { private: static void impl2(Foo* f) { ... } public: Baz() { MyFunc = impl2; } ... }; 然后仅覆盖abort()将为您提供调用final的能力,这将比尝试virtual bool HasHoof() { return false; }更快。这是因为bool Horse::HasHoof() { return true; }在某些情况下甚至必须递归地遍历类层次结构,以查看是否可以从实际指针类型和所需的类类型构建路径。虽然虚拟调用始终是相同的 - 取消引用两个指针。


2
投票

这是现代C ++中虚拟表的可运行手动实现。它有明确定义的语义,没有hacks,也没有if (anim->HasHoof())

注意:if(dynamic_cast<Horse*>(anim))dynamic_castvoid*.*不同。成员函数指针的工作方式不同。

->*

1
投票

每个对象都有一个vtable指针,指向一个成员函数数组。

© www.soinside.com 2019 - 2024. All rights reserved.