【问题标题】:Pointers to virtual member functions. How does it work?指向虚拟成员函数的指针。它是如何工作的?
【发布时间】:2010-11-08 10:12:48
【问题描述】:

考虑以下 C++ 代码:

class A
{
public:
      virtual void f()=0;
};


int main()
{
     void (A::*f)()=&A::f;
}

如果我不得不猜测,我会说 &A::f 在这种情况下意味着“A 的 f() 实现的地址”,因为指向常规成员函数的指针和虚成员函数。而且由于 A 没有实现 f(),这将是一个编译错误。然而,事实并非如此。

不仅如此。以下代码:

void (A::*f)()=&A::f;
A *a=new B;            // B is a subclass of A, which implements f()
(a->*f)();

实际上会调用 B::f。

这是怎么发生的?

【问题讨论】:

  • 因为编译器让它发生了!如果调用普通方法与调用虚拟方法没有什么不同,为什么您认为使用方法指针时代码有任何不同。您认为编译器将普通方法(虚拟和普通)调用翻译成什么?

标签: c++ virtual member-function-pointers


【解决方案1】:

它之所以有效,是因为标准规定这就是它应该发生的方式。我用 GCC 做了一些测试,结果发现对于虚函数,GCC 以字节为单位存储了相关函数的虚表偏移量。

struct A { virtual void f() { } virtual void g() { } }; 
int main() { 
  union insp { 
    void (A::*pf)();
    ptrdiff_t pd[2]; 
  }; 
  insp p[] = { { &A::f }, { &A::g } }; 
  std::cout << p[0].pd[0] << " "
            << p[1].pd[0] << std::endl;
}

该程序输出1 5 - 这两个函数的虚拟表条目的字节偏移量。它遵循 Itanium C++ ABIwhich specifies that

【讨论】:

  • 我想我的问题的答案不是 C++ 标准化的。然而,vtables 也是如此,但我不知道有任何编译器不使用 vtables 作为虚函数的机制,所以我想也有一个 standard 机制。你的回答只会让我更加困惑。如果编译器将 1 和 5 存储在指向 A 的成员函数的指针中,它如何判断这是一个 vtable 索引还是一个真实地址? (注意,指向常规成员函数和虚成员函数的指针没有区别)
  • 为什么这个答案让你更加困惑?有什么不清楚的就问吧。这种东西不规范。由实施来考虑解决它的方法。他们可以决定它是否是函数指针:我认为这就是他们加 1 的原因。因此,如果数字未对齐,则它是 vtable 偏移量。如果它是对齐的,它是一个指向成员函数的指针。不过,这只是我的猜测。
  • 谢谢,这听起来合乎逻辑。但是,对于 C++ 实现来说,效率有点低……检查了 VC 上的代码,结果完全不同。输出是'c01380 c01390',看起来像是某个东西的地址。
  • 查看@onebyone 链接的快速代表文章。它包含有关其他编译器的好信息。我的回答当然是 GCC 特定的。
  • 标准说这必须有效。因此,符合标准的编译器将使其工作。在 gcc 之外的其他实现中,虚函数指针可以是 16 字节。
【解决方案2】:

这里有太多关于成员函数指针的信息。在“The Well-Behaved Compilers”下有一些关于虚函数的内容,尽管当我阅读 IIRC 文章时,我正在略读那部分,因为这篇文章实际上是关于在 C++ 中实现委托。

http://www.codeproject.com/KB/cpp/FastDelegate.aspx

简短的回答是它取决于编译器,但一种可能性是成员函数指针被实现为一个结构,其中包含一个指向进行虚拟调用的“thunk”函数的指针。

【讨论】:

  • 嗨,你刚刚提到了 thunk。有什么好的文章解释 thunk 吗?
  • “thunk 这个词指的是一段低级代码,通常是机器生成的,它实现了特定软件系统的一些细节。”,来自en.wikipedia.org/wiki/Thunk。该页面讨论了几种 thunk,虽然不是这个特定的。
【解决方案3】:

我不完全确定,但我认为这只是常规的多态行为。我认为 &amp;A::f 实际上是指类的 vtable 中函数指针的地址,这就是为什么你没有得到编译器错误的原因。 vtable 中的空间仍然被分配,这就是你实际返回的位置。

这是有道理的,因为派生类本质上会用指向其函数的指针覆盖这些值。这就是为什么 (a-&gt;*f)() 在您的第二个示例中起作用的原因 - f 正在引用在派生类中实现的 vtable。

【讨论】:

  • 如果指向常规成员函数的指针和指向虚拟成员函数的指针之间存在分离,则可能会出现这种情况。然而,正如我所提到的,没有,这就是它的全部意义所在。
  • 绝对允许编译器将所有方法(无论是否虚拟)都放在 vtable 中。如果这样做,它就可以使用 vtable 索引作为指向成员函数的指针。实际上对于编译器来说相当简单 - 只需确保非虚拟覆盖器获得自己的 vtable 条目而不是覆盖基类条目。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-31
  • 1970-01-01
  • 2011-07-01
  • 2015-12-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多