调用另一个相关对象的受保护虚函数(用于代理)

Posted

技术标签:

【中文标题】调用另一个相关对象的受保护虚函数(用于代理)【英文标题】:Calling protected virtual function of another related object (for proxying) 【发布时间】:2014-10-08 11:42:16 【问题描述】:

所以一个任务:我们有一个第三方库,有一个类(叫它Base)。库提供了一个隐藏的实现,称为 Impl。 我需要写一个代理。不幸的是 Base 有一个受保护的虚函数 fn。

所以问题是从 C++ 的角度来看,下面的代码在多大程度上是正确的?它目前在 Visual Studio 中完美运行,并且在 Mac 上的 clang/gcc 中无法运行(但编译时没有任何警告)。我完全意识到那里发生的机制,所以如果删除类问题,一切都可以在两个平台上运行。我想知道我是否应该向 clang 报告错误,或者它是 C++ 标准的未定义/未指定行为。

代码的预期结果是正常调用Impl::fn()

class Base

protected:
    virtual void fn()
;

class Impl : public Base

public:
    Impl() : mZ(54)
protected:

    virtual void fn()
    
        int a = 10; ++a;
    

    int mZ;
;

class Problem

public:
    virtual ~Problem()
    int mA;
;

class Proxy :  public Problem, public Base

public:
    virtual void fn()
    
        Base * impl = new Impl;

        typedef void (Base::*fn_t)();
        fn_t f = static_cast<fn_t>(&Proxy::fn);
        (impl->*f)();

        delete impl;
    
;

int main()

    Proxy p;
    p.fn();

【问题讨论】:

【参考方案1】:

它恰好在这一行崩溃:

    (impl->*f)();

试图访问分配块后面的内存。这通常暗示一个人没有正确设置this,事实上,交换继承顺序解决了这个问题,证实了这一理论。

    Base * impl = new Impl;

    typedef void (Base::*fn_t)();
    fn_t f = static_cast<fn_t>(&Proxy::fn);
    (impl->*f)();

所以问题实际上是 fn_t 指向的位置(当然不是 Base::fn 这里的 vtable 条目)。

现在我们真正看到了这个问题。您尝试调用另一个对象的受保护函数,尝试使用 &Base::fn 是不可能的,尝试使用指向 Proxy::fn 的指针实际上是一个不同的函数,具有不同的 vtable 索引,它不会存在于 Base 中。

现在这工作只是因为 MSVC 使用不同的内存布局,巧合的是 Proxy::fn 和 Base::fn 具有相同的 vtable 索引。尝试在 MSVC 构建中交换继承顺序,它可能会崩溃。或者尝试在某处添加另一个函数或成员,我猜它迟早与 MSVC 崩溃。

关于基本思想:我们在这里尝试完成的是调用一个不同对象的受保护函数。参考this列表,基本上和here说的一样

声明为受保护的类成员只能由以下人员使用:

    最初声明这些成员的类的成员函数。 最初声明这些成员的类的朋友。 从最初声明这些成员的类派生的具有公共或受保护访问权限的类。 直接私有派生类也可以私有访问受保护的成员。
    并非如此 没有朋友声明 试图在不同的对象上调用方法,而不是this 并非如此

所以我认为这是不合法的,导致未定义的行为,对任何巧妙的转换等都漠不关心。

【讨论】:

谢谢,我知道它是如何工作的以及它为什么会崩溃。它不会与 MSVC 一起崩溃。我想知道的是这是否是一个正确的(虽然有点奇怪)c++ 代码,它必须在任何地方工作。顺便说一句,我很高兴你意识到微妙之处在哪里以及它应该如何工作:) 我不认为这是定义明确的行为,因为您实际上是在调用 不同 对象的受保护函数,这是不合法的。 @dev_null 你是什么意思正确?您故意规避标准来做被指定为非法的事情。当然,标准的重点不是“必须在任何地方工作”:定义必须工作的内容。 tl;博士:没有。 关于下一次编辑后的视觉工作室:不,它独立于类的顺序和虚拟功能的数量。恕我直言,这是正确的。 VS 使用另一个模型来表示 pmfs(指向成员函数的指针)。关于调用受保护函数:如果对象属于同一类型,我可以在另一个对象上调用它。即调用 mMyOtherProxyObject 的 Proxy::fn() 是完全可以的。这里最重要的是指针的转换(从 Proxy 到 Base 以及从 Base 到 Impl)。我什至可以改变问题:如果这里的一切都是“公开的”怎么办。该示例应该有效吗? 好吧,如果一切都是公开的,你就可以直接通过基指针调用函数...【参考方案2】:

问题是您从BaseProblem 中多重继承。类的 ABI 布局不是由标准定义的,实现可以选择如何布局对象,这就是为什么在不同的编译器上会看到不同的结果。

具体来说,崩溃的原因是您的派生类最终有两个 v-table:BaseProblem 各一个。

我是 g++ 的情况,因为你继承了 public Problem, public Base,所以类布局在“传统”位置有 Problem 的 v-table,在类布局后面有 Base 的 v-table。

如果您想查看此操作,请将其添加到您的 main...

int main()

    Proxy p;
    Base *base = &p;
    Problem *problem = &p;
    std::cout << "Proxy: " << &p << ", Problem: " << problem << ", Base: " << base << '\n';

你会看到类似这样的东西...

Proxy: 0x7fff5993e9b0, Problem: 0x7fff5993e9b0, Base: 0x7fff5993e9c0

现在,你在这里做了一些“邪恶”的事情:

typedef void (Base::*fn_t)();
fn_t f = static_cast<fn_t>(&Proxy::fn);
(impl->*f)();

因为您正在获取Proxy 的成员函数指针并将其应用于Impl 对象。是的,它们都继承自Base,但是你给了它一个类Proxy 的成员函数指针,当它查找那个v-table 时,它​​们位于不同的位置。

您真的只想获取Base 的成员函数指针,但由于您是在Proxy 的上下文中执行此操作,因此您只能访问Proxy 成员函数。现在应该很明显,由于多重继承,这是不可取的。

但是,您可以通过一个小助手类轻松获得我认为您想要的内容...

virtual void fn()

    typedef void (Base::*fn_t)();
    struct Helper : Base 
      static fn_t get_fn()  return &Helper::fn; 
    ;

    Base * impl = new Impl;
    fn_t f = Helper::get_fn();
    (impl->*f)();
    delete impl;

因为Helper继承自Base,所以它可以访问受保护的成员,你可以在Proxy的多重继承上下文之外访问它。

【讨论】:

谢谢,因为代理在我身边,我可以选择继承顺序。以及我意识到多重继承的问题。主要问题是代码的正确程度(独立于编译器和继承顺序)。我有权利期望这将适用于例如英特尔编译器,或 10 年后在另一个 CPU/平台上的 C++ 编译器。 你根本不能对它做任何假设,除非你做你正在做的事情可能会与不同的编译器一起破坏。帮助程序解决方法应该提供您想要的任何编译器。

以上是关于调用另一个相关对象的受保护虚函数(用于代理)的主要内容,如果未能解决你的问题,请参考以下文章

C++的构造函数为何不能为虚函数

如何能避免在调用子类对象的虚函数时调用父类的虚函数呢?

C++学习24 虚析构函数

虚函数的小秘密

C++ 内存布局:深入理解C++内存布局

C++中虚函数工作原理和(虚)继承类…