为什么在构造函数中调用虚拟方法并绑定虚拟方法,然后稍后调用它会产生不同的结果?

Why does invoking a virtual method in constructor and binding a virtual method then calling it later yield different results?

提问人:codesavesworld 提问时间:9/25/2023 最后编辑:Jan Schultkecodesavesworld 更新时间:9/26/2023 访问量:141

问:

这是我的代码片段:

class Base {
  public:

  Base() {
    foo();
    bind();
  }

  virtual void foo() {
    std::cout << "base foo\n";
  }

  void bind() {
    fn = std::bind(&Base::foo, this);
  };

  std::function<void()> fn;
};

class Derived : public Base {
  public:

  void foo() override {
    std::cout << "derived foo\n";
  }

  void bind()  {
  }

  int val;    
};

int main() {
  Base* p = new Derived();
  p->fn();
}

输出为:

base foo
derived foo

foo()prints ,因为此时 vtable 仍然指向 ,根据这个问题下的答案。base fooBase::foo

在构造过程中,对象还不是类。那么,调用时指针还是指向类的指针,而参数指针是在构造函数体中传递的,那么为什么要在类中调用呢?BaseDerivedstd::bind()thisBaseBasep->fnfooDerived

我的编译器是 Apple clang 版本 14.0.3

C++ 构造函数 Virtual-Functions Stdbind

评论

2赞 Sam Varshavchik 9/25/2023
乍一看,这可能看起来令人困惑。不要气馁。继续插入,并尝试弄清楚虚拟函数在 C++ 中是如何工作的。一旦你弄清楚并理解了一切,你就会感到敬畏和惊讶,因为你看到了强大的 C++ 虚拟函数的力量。
1赞 Marek R 9/25/2023
std::function在这里并不重要。整个事情是由事实引起的,对象是如何一步一步地构建的。有 std::function没有它
4赞 Drew Dormann 9/25/2023
换句话说,一旦调用了构造函数,每个指向 Base 的指针现在都是指向 Derived 的指针。从该位置访问的 vtable 是派生的 vtable。这就是动态调度的“动态”部分。Derived
1赞 YSC 9/25/2023
@DrewDormann,这就是重点,应该是一个答案。
4赞 Fareanor 9/25/2023
@MarekR godbolt.org/z/d5qbGP53s 而且这不仅适用于 lambda。每次您在实现中调用完全限定时,它也会绕过虚拟调度(但我想您已经知道了)。Base::foo()Derived

答:

3赞 Drew Dormann 9/25/2023 #1

当调用 std::bind 时仍然是指向 Base 类的指针,并且参数指针是在 Base 的构造函数体中传递的,为什么 p->fn 在派生类中调用 foo?

调用构造函数后,指向 的每个指针现在都是指向 Derived 的指针。从该位置访问的 vtable 现在是派生的 vtableDerivedBase

这就是动态调度的“动态”部分。

评论

0赞 Ben Voigt 9/25/2023
更准确地说,每个指向的指针现在都是指向“子对象”的指针(它可能不包含BaseBaseDerivedDerived)
0赞 Drew Dormann 9/25/2023
@codesavesworld vtable 指针在调用派生构造函数之前立即更改。
6赞 Ben Voigt 9/25/2023 #2

std::bind(或指向成员函数的指针的任何其他用法,其中所讨论的成员函数是)不绑定到特定函数。相反,当与虚拟成员函数一起使用时,它会绑定到虚拟调度槽1(即使用 vtable 的常见实现上的 vtable 槽)。virtual

因此,在 vtable 被更多派生构造函数重新配置后,通过结果函子找到的函数会发生变化。bind


1 指针到成员函数类型很胖有几个原因,它包含的不仅仅是一个代码地址,并且需要能够虚拟调度就是其中之一。

2赞 molbdnilo 9/25/2023 #3

只是因为我有点无聊,并且为了表明不涉及魔法,这里有一个非常简化(和有效)的插图,说明事情在幕后是如何运作的(现实世界中还有很多事情发生,但原理是一样的):

#include <iostream>

struct Base;

// Roll our own "bind + std::function" simplification.
struct Bind {
    Base* b;   // "this"
    size_t fn; // Index in function table.
    void operator()();
};

using member_fn = void(*)(Base*); // Limit to one type of member function.

struct Base
{
    member_fn* v_table;
    Bind fn;
};

void Bind::operator()() { b->v_table[fn](b); }

struct Derived : Base {};

void Base_foo(Base* self) { std::cout << "base foo\n"; }
void Base_bind(Base* self) { self->fn = { self, 0 }; } // foo is the first function.
member_fn Base_vtable[] = { Base_foo, Base_bind };

void Derived_foo_impl(Derived* self) { std::cout << "derived foo\n"; }
void Derived_foo(Base* self) { Derived_foo_impl(static_cast<Derived*>(self)); }
void Derived_bind_impl(Base* self) {}
void Derived_bind(Base* self) { Derived_bind_impl(static_cast<Derived*>(self)); }
member_fn Derived_vtable[] = { Derived_foo, Derived_bind };

void init_base(Base* self)
{
    self->v_table = Base_vtable;
    self->v_table[0](self); // foo()
    self->v_table[1](self); // bind()
}

void init_derived(Derived* self)
{
    init_base(static_cast<Base*>(self));
    self->v_table = Derived_vtable; // Now we are not a Base any more...
}

int main()
{
    Derived d;
    init_derived(&d);
    Base* b = static_cast<Base*>(&d);
    b->fn();
}