msvc 中的 C++ 访问冲突但不是 gcc 中的多重继承和强制转换

C++ access violation in msvc but not gcc for multiple inheritance and cast

我有一个相当大的项目,有多个接口和实现。 该代码是在 linux 环境中使用 g++(我认为是 5.4)实现的。在使用 VS15 (MSVC v140) 将代码移植到 Windows 和 运行 之后,我在尝试访问强制转换的指针后遇到了访问冲突。

这是下面代码中的继承层次结构:

                 A    
                / \  
     virtual   /   \   
              /     |
             B      |
             |      | virtual
             C      |
             |      |
             \      / 
              \    /
                D

在真正的代码中,继承设计包含更多类 所以请不要挑剔为什么这是我从A 继承的方式。 我已经缩小了代码范围,只显示必要的内容。

以下使用 gcc 运行并打印 foo called 两次 (Live demo on rextester), but with msvc on the second call to foo crashes with access violation (Live demo on rextester)

#include <iostream>

class A{};

class B : public virtual A{};

class C : public B
{
public:
    virtual void foo() = 0;
};

class D : public virtual A, public C
{
public:
    bool convert(int id, B** ext)
    {
        if (id == 1)
        {
            *ext = (C*)this;
            return true;
        }

        if (id == 42)
        {
            C** pp_ext = (C**)(ext);
            *pp_ext = (C*)this;
            return true;
        }

        return false;
    }
    void foo() override
    {
        std::cout << "foo called" << std::endl;
    }
};


int main()
{
    D s;
    C* foo_ext = nullptr;
    s.convert(42, (B**)&foo_ext);
    foo_ext->foo();

    foo_ext = nullptr;
    s.convert(1, (B**)&foo_ext);
    foo_ext->foo();

    return 0;
}

首先 - 我是否遗漏了 *ext = (C*)this; 转换中的基本错误?

第二 - 为什么这段代码在两个编译器中不同?

编辑:

  1. 此代码使用指针,指向指针的指针,并出于充分的理由使用此继承构建(其中之一是 ABI 兼容接口)。

  2. dynamic_cast 不会改变这种情况下的行为。

  3. 如果我在 *ext = (C*)this; 之后调用 static_cast<C*>(*ext)->foo();,它将调用 foo,但在从 convert 返回后失败。这是我已经理解的东西,这让我明白 42 的解决方案是一个(好?)解决方案。

在处理继承,尤其是多重继承时,您真的应该尽量完全避免强制转换。但是如果你必须投射,那么使用 static_castdynamic_cast。这样编译器将帮助您避免无效的转换。如果你做任何其他事情,那么你必须了解 C++ 的所有细节,甚至比编译器本身更好!否则你很容易犯错误。就像你在这里所做的那样。

尝试将您的 main 修改为:

int main()
{
    D s;

    A* a = &s;
    B* b = &s;
    C* c = &s;
    std::cout << "A address = " << a << std::endl;
    std::cout << "B address = " << b << std::endl;
    std::cout << "C address = " << c << std::endl;
    std::cout << "D address = " << &s << std::endl;

    C* foo_ext = nullptr;
    s.convert(42, (B**)&foo_ext);
    std::cout << "foo_ext = " << foo_ext << std::endl;
    foo_ext->foo();

    foo_ext = nullptr;
    s.convert(1, (B**)&foo_ext);
    std::cout << "foo_ext = " << foo_ext << std::endl;
    foo_ext->foo();

    return 0;
}

运行 在崩溃之前,我得到:

A address = 0037FEA0
B address = 0037FE9C
C address = 0037FE98
D address = 0037FE98
foo_ext = 0037FE98
foo called
foo_ext = 0037FE9C

显然在第二种情况下 foo_ext 没有设置为对象的正确 C 地址,而是 B 部分。那么在实践中,对 foo() 的调用可能会通过不正确的或 non-existent 虚拟 table 指针,因此会崩溃。

现在,为什么第一个案例有效?好吧,将它减少到最低限度,你已经有效地完成了:

C* foo_ext = nullptr;
C** ppc = &foo_ext;
B** ppb = (B**)ppc;
C** pp_ext = (C**)ppb;
*pp_ext = &s;

所以它以一个C指针开始,以一个C指针结束。并且编译器知道如何正确地将 D 指针转换为 C 指针。 (派生为基本转换。编译器不需要强制转换来执行此操作。)

但是在第二种情况下你实际上有:

C* foo_ext = nullptr;
C** ppc = &foo_ext;
B** ext = (B**)ppc;
*ext = &s;

所以在最后一行,编译器将 D 指针移动为 B 指针。但实际上该地址以 C 指针结束。所以它在继承层次结构中移得太远了!


现在,如何修复您的程序...好吧,基本思想当然是摆脱大部分转换并用静态或动态转换替换剩余的几个位置。尝试以下操作:

#include <iostream>

class A {};

class B : public virtual A {};

class C : public B
{
public:
    virtual void foo() = 0;
};

class D : public virtual A, public C
{
public:
    bool convert(int id, B** ext)
    {
        if (id == 1)
        {
            *ext = this;
            return true;
        }

        if (id == 42)
        {
            *ext = this;
            return true;
        }

        return false;
    }
    void foo() override
    {
        std::cout << "foo called" << std::endl;
    }
};


int main()
{
    D s;

    A* a = &s;
    B* b = &s;
    C* c = &s;
    std::cout << "A address = " << a << std::endl;
    std::cout << "B address = " << b << std::endl;
    std::cout << "C address = " << c << std::endl;
    std::cout << "D address = " << &s << std::endl;

    B* b_pointer = nullptr;
    C* foo_ext = nullptr;
    s.convert(42, &b_pointer);
    foo_ext = static_cast<C*>(b_pointer);
    std::cout << "foo_ext = " << foo_ext << std::endl;
    foo_ext->foo();

    b_pointer = nullptr;
    foo_ext = nullptr;
    s.convert(1, &b_pointer);
    foo_ext = static_cast<C*>(b_pointer);
    std::cout << "foo_ext = " << foo_ext << std::endl;
    foo_ext->foo();

    return 0;
}

这应该 运行 符合所有预期的输出。静态转换允许编译器根据它们的类型正确定位指针。 (在这个有限的示例中,我们当然知道所有类型的真正含义,因此我们不必为动态转换而烦恼。)

我假设这个例子是基于一个现实世界的问题。我不能说这个确切的解决方案是否适合(因为显然我不知道真实情况)。但原则是合理的:不要通过强制有问题的转换来欺骗编译器。尽可能让它自动转换,必要时只求助于 static_castdynamic_cast

第一个运行时问题出现在:

s.convert(42, (B**)&foo_ext);

foo_ext 的类型为 C *。因此,在 convert 中使用 *ext 会违反严格的别名违规行为,因为它会像访问 B * 一样访问 C * 的内存。通常,指向不同类型的指针可能具有不同的大小和表示形式;但即使他们不这样做,仍然不允许给他们起别名。

尽管严格的别名规则在访问基 classes 时有一个例外,但不会扩展到基 class 指针。

MSVC 可能不会在那种情况下强制执行严格的别名(事实上 Windows API 中的某些事情依赖于这种行为)。但是如果你想编写可移植的代码,最好不要依赖严格的别名违规。


convert函数使用pass-by-pointer。在 C++ 中从来不需要这样做。您可以使用 pass-by-reference 作为语言功能。这减少了出错的机会——您的代码中的一些错误实际上甚至无法用引用符号表示。

这是修改后的版本:

#include <iostream>

class A{};

class B : public virtual A{};

class C : public B
{
public:
    virtual void foo() = 0;
};

class D : public virtual A, public C
{
public:
    bool convert(int id, B*& ext)
    {
        if (id == 1)
        {
            ext = static_cast<C *>(this);  // note: redundant cast
            return true;
        }

        if (id == 42)
        {
            ext = this;
            return true;
        }

        return false;
    }
    void foo() override
    {
        std::cout << "foo called" << std::endl;
    }
};

int main()
{
    D s;
    B* foo_ext_b;
    C* foo_ext;

    foo_ext_b = nullptr;

    if ( !s.convert(42, foo_ext_b) )
        throw std::runtime_error("convert42 failed");

    foo_ext = static_cast<C *>(foo_ext_b);
    foo_ext->foo();

    foo_ext_b  = nullptr;

    if ( !s.convert(1, foo_ext_b) )
        throw std::runtime_error("convert1 failed");

    foo_ext = static_cast<C *>(foo_ext_b);
    foo_ext->foo();

    return 0;
}

请注意,通常使用 foo_ext = static_cast<C *>(foo_ext_b); 是 error-prone。如果 convert "returned" a B * 没有恰好指向 B,而 B 是 [=23] 的基础 class,则会出现未定义行为=]实例.

为了安全起见,您可以使用 dynamic_cast。但是要让 dynamic_cast 工作,基础 class 必须至少有一个虚函数。您可以将虚拟析构函数添加到 BA.