2

所以一个任务:我们有一个第三方库,有一个类(叫它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();
}
4

2 回答 2

1

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

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

我是 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 表时,它们位于不同的位置。

您真的只是想获取成员函数指针,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;
}

因为HelperBase它继承可以访问受保护的成员,并且您可以在Proxy.

于 2014-10-08T13:57:55.953 回答
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 一起崩溃。

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

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

  1. 最初声明这些成员的类的成员函数。
  2. 最初声明这些成员的类的朋友。
  3. 从最初声明这些成员的类派生的具有公共或受保护访问权限的类。
  4. 直接私有派生类,它们也对受保护成员具有私有访问权限。
  1. 不是这样
  2. 没有朋友声明
  3. 试图在不同的对象上调用方法,而不是this
  4. 不是这样

所以我不认为这是合法的,导致不确定的行为,对任何聪明的演员都漠不关心等。

于 2014-10-08T12:00:16.290 回答