【问题标题】:How is pointer to member function implemented in C++?指向成员函数的指针是如何在 C++ 中实现的?
【发布时间】:2015-12-19 04:49:05
【问题描述】:

c++中指向成员函数的指针分为三部分:

Offset
Address/index
virtual?

使用base pointer.调用派生对象时,偏移量用于指针调整

这个偏移是如何实现的?它是指向某个表的指针吗,每个派生类一个表,并且该表包含(base X, offset) 形式的条目?

另外,我可以从哪里获得有关此的更多信息?

【问题讨论】:

  • 这取决于实现。没有通用的答案。
  • @RaymondChen 好的,如果你能告诉我们任何实现的细节,那就太好了。我不是在寻找通用解决方案。
  • "The Fastest Possible C++ Delegates" 的文章中讨论了成员函数指针的各种实现,这相当容易理解,此外还有已经融入该代码的可交叉编译代码。

标签: c++ pointers function-pointers multiple-inheritance


【解决方案1】:

首先您应该注意,C++ 方法可以像常规函数一样被实现(并且通常被实现),它在所有其他参数之前接受一个额外的隐藏参数,名为 this

换句话说,在

struct P2d {
   double x, y;
   void doIt(int a, double b) {
       ...
   }
};

doIt 的机器代码与 C 编译器生成的相同

void P2d$vid$doIt(P2d *this, int a, double b) {
    ...
}

p->doIt(10, 3.14) 这样的调用被编译为P2d$vid$doIt(p, 10, 3.14);

给定一个没有虚方法的简单类的方法指针可以实现为指向方法代码的常规指针(注意:我使用 vid 表示“Void of Int +Double”作为 C++ 编译器用于处理重载的“名称修改”的玩具示例 - 具有相同名称但参数不同的不同函数)。

如果该类有虚方法,但事实并非如此。

大多数 C++ 编译器都实现了虚拟调度,不使用 VMT... 即在

struct P2d {
    ...
    virtual void doIt(int a, double b);
};

p->doIt(10, 3.14) 这样的调用代码,其中pP2d *,与C 编译器生成的代码相同

(p->$VMTab.vid$doIt)(p, 10, 3.14);

即该实例包含一个指向每个成员包含有效代码地址的虚拟方法表的隐藏指针(假设编译器无法推断 p 的类确实是 P2d 而不是派生的,因为在这种情况下调用可以与非虚拟方法相同)。

需要方法指针来尊重虚拟方法...即使用从P2d派生的实例上的方法指针间接调用doIt需要调用派生版本,而相同的方法指针是调用基在 P2d 实例上使用时的版本。这意味着选择调用哪个代码取决于指针和类实例。

一个可能的实现是使用蹦床:

void MethodPointerCallerForP2dDoit(P2d *p, int a, double b) {
    p->doIt(a, b);
}

在这种情况下,方法指针仍然只是指向代码的指针(但指向蹦床,而不是指向最终方法)。

另一种方法是将方法的 index 作为方法指针存储在 VMT 内。这是可行的,因为在 C++ 中,方法指针与特定的类相关联,因此编译器知道该类是否存在虚方法。

多重继承不会使方法指针变得复杂,因为一切都可以在编译时解析为一个最终的 VMT 表。

【讨论】:

  • p->P2d$VMTab[P2d$Index$doIt$vdd] 需要澄清。
  • @black:使用结构而不是数组简化了 VMT 解释(实际上,指针/索引版本还需要强制转换才能成为有效的 C 代码)。
  • @6502 您建议的方式,不需要偏移量。但是为什么方法指针的大小比 g++ 中的普通函数指针大 1 个字节?
【解决方案2】:

这是关于 6502 答案的评论,但我缺乏声誉。

没有虚方法的简单类的方法指针可以实现为指向该方法的常规指针

我认为这种说法是不正确的,因为多重继承使事情变得复杂。考虑这段代码:

struct A {
    int a;
    void f() {
        // use a
    }
};

struct B {
    int b;
    void g() {
        // use b
    }
};

// C objects may look like this in memory:
//
//     |-----|
//     |  A  |
//     |-----|
//     |  B  |
//     |-----|
//
// Since only one of A and B can be at the start of C in terms of memory
// layout, at least one of the following can't work:
//
// * naively interpreting a pointer to C as a pointer to A
// * naively interpreting a pointer to C as a pointer to B
struct C : A, B {};

void call_ptm(void (C::*ptm)()) {
    C c;
    // `ptm` could be `A::f` or `B::g`.  We don't know.  At what offset
    // relative to `this` do we expect to find data members then?  It depends
    // on whether `ptm` points to `A::f` or `B::g`, which isn't known at
    // compile time.
    (c.*ptm)();
}

无论如何,指向成员函数的指针需要存储一个偏移量,该偏移量可以在调用时应用于this

【讨论】:

  • “天真地将指向 C 的指针解释为指向 B 的指针”不起作用,但是将 B::*ptb 分配给 C::*ptc 可以调整偏移量。以同样的方式赋值B * b = new C调整指针的值,使其指向B子对象
  • 但是这个偏移量仍然必须存储在某个地方,这就是为什么我反对“一个没有虚方法的简单类的方法指针可以实现为该方法的常规指针”的断言"。
  • 是的,它存储在程序的指令中。这是一个静态值
  • this 指针偏移量如何取决于void (C::*ptm)() 是否持有指向AB 成员函数的指针?这在编译时是未知的。
  • 它包含一个指向C 的成员函数的指针,该指针在编译时是已知的。 void (B::*ptb)()转换 不保值
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-24
  • 1970-01-01
相关资源
最近更新 更多