【问题标题】:Virtual Calls using address of pure virtual member. Is it legal?使用纯虚拟成员的地址进行虚拟调用。合法吗?
【发布时间】:2009-06-09 06:48:06
【问题描述】:

我曾经读过(可能在 c.l.c++.moderated 上),虚拟函数调用可以被模板化。我尝试了以下几行。

#include <iostream>

template<class T, class FUN> 
void callVirtual(T& t, FUN f){ 
   (*t.*f)(); 
} 


struct Base{ 
   virtual ~Base(){} 
   virtual void sayHi()=0; 
}; 


struct Derived : public Base{ 
   void sayHi(){ 
      std::cout << "Hi!" << std::endl; 
   } 
}; 


void Test(){ 
   Base* ptr = new Derived; 
   callVirtual(ptr,&Base::sayHi); 
} 

int main()
{
   Test();
   return 0;
}

Output:
Hi!

模板化方法虽然在编译时给出了纯虚拟基成员方法的地址,但在运行时调用了正确的方法。 在标准 C++ 中获取纯虚拟成员的地址是否合法?

提前致谢

EDIT-1:我删除了问题的第二部分“它是如何工作的?”。看起来这就是引起人们注意的地方。

EDIT-2:我搜索了 c.l.c++.moderated 并发现了这个 link (http://groups.google.com/group/comp.lang.c++.moderated/browse_thread/thread/5ddde8cf1ae59a0d)。共识似乎因为标准没有限制它,所以它是有效的。

EDIT-3: 在阅读了 codeproject 文章(感谢 ovanes)之后,我认为编译器做了一些魔术。由于虚函数是通过 vtable(特定于编译器)实现的,因此获取虚函数的地址总是会给出 vtable 中的偏移量。根据使用的“this”指针,调用适当的函数(其地址位于偏移量处)。我不确定如何证明这一点,因为标准并没有说明任何内容!

【问题讨论】:

    标签: c++ inheritance templates


    【解决方案1】:

    确实如此。您的代码与像这样调用纯虚方法没有什么不同:

    void Test()
    { 
       Base* ptr = new Derived; 
       ptr->sayHi();
       delete ptr;
    } 
    

    唯一的区别是您有另一种调用机制,在本例中是通过 callVirtual()。

    【讨论】:

    • 啊,这另一种机制涉及获取纯虚拟成员函数的地址。这是我主要关心的。
    【解决方案2】:

    正如 Magnus Skog 所说,模板部分并不真正相关。归根结底是:

    (ptr->* &Base::sayHi)()
    

    似乎有效,但是

    ptr->Base::sayHi()
    

    显然不是,因为 sayHi 是纯虚拟的。

    我无法在标准中找到任何内容,说明当您获取虚拟或纯虚拟函数的地址时会发生什么。我不确定这是否合法。不过它在 GCC 和 MSVC 中工作,而且 Comeau 的在线编译器也不会抱怨。

    编辑

    即使它是有效的,正如你的编辑所说,我仍然想知道它是什么意思。

    如果我们为简单起见假设sayHi 是非纯的(因此存在Base::sayHi 的定义),那么如果我获取它的地址会发生什么?是获取Base::sayHi的地址,还是vtable指向的函数地址(本例为Derived::sayHi)?

    显然,编译器假定后者,但为什么呢? 在基类中调用ptr-&gt;Base::sayHi() 调用sayHi,但获取Base::sayHi 的地址给了我Derived::sayHi 的地址

    这对我来说似乎不一致。我错过了这背后的一些理由吗?

    【讨论】:

    • 我也很困惑。 C++ 继续让我感到困惑:-(
    • "如果我获取它的地址会怎样?" 你获取它的名称。指向成员的指针都是关于名称的。
    【解决方案3】:

    下面的这篇文章广泛讨论了 C++ 中的成员函数指针、它们是如何实现的以及缺陷在哪里。它还处理虚拟成员函数指针等等。我想它会回答你所有的问题。

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

    它还展示了如何在 C++ 中实现委托以及您可能陷入的陷阱。

    致以诚挚的问候,

    欧文

    【讨论】:

    • 精彩的阅读,谢谢。看起来我得到了答案(参见edit-3)。
    【解决方案4】:

    我猜它是未定义的。我在规范中找不到任何内容。

    虚拟方法是使用称为vtable 的概念实现的。

    我会说它是特定于编译器实现的。我真的认为它是纯虚拟的并不重要,如果它也是虚拟的也会发生同样的情况。

    我刚刚使用 Visual Studio 2008 编译了您的代码并反汇编了 exe。 VS2008 所做的是创建一个 thunk 函数,使用传入的 'this' 指针跳转到 vtable 条目。

    这是 callVirtual 模板函数的设置和调用。

    push    offset j_??_9Base@@$B3AE ; void (__thiscall *)(Base *)
    lea     eax, [ebp+ptr]
    push    eax             ; Base **
    call    j_??$callVirtual@PAUBase@@P81@AEXXZ@@YAXAAPAUBase@@P80@AEXXZ@Z ; callVirtual<Base *,void (Base::*)(void)>(Base * &,void (Base::*)(void))
    

    所以它将函数指针传递给 thunk 函数:j_??_9Base@@$B3AE

    ; void __thiscall Base___vcall_(Base *)
    j_??_9Base@@$B3AE proc near
    jmp     ??_9Base@@$B3AE ; [thunk]: Base::`vcall'{4,{flat}}
    j_??_9Base@@$B3AE endp
    

    thunk 函数所做的只是使用 vtable 跳转到真正的类方法。

    【讨论】:

    • 谢谢。您知道 C++ 标准的一部分,其中提到了指向纯虚拟的成员函数指针。我试图找到但无法找到任何具体细节。在我看来,由于允许指向不完整类型的成员指针,因此也允许指向纯虚拟的成员指针。
    • +1 您编辑的答案接近此问题的“答案”:-)。我开始认为处理虚函数(无论是否纯)并调用它更倾向于语言的实现定义方面。
    • 令人惊讶的是有多少没有定义并留给编译器的实现者来选择他们想要的任何实现。
    • 答案不正确。 OP 的代码绝对没有任何未定义或实现定义的内容。代码有效且行为由标准定义。反汇编编译的代码在这里无济于事。编译器只是以一种可能的方式实现标准行为。
    猜你喜欢
    • 1970-01-01
    • 2018-07-09
    • 1970-01-01
    • 2017-05-18
    • 2011-03-05
    • 1970-01-01
    • 2011-08-20
    • 2010-11-21
    相关资源
    最近更新 更多