在C++中,什么情况下进行向上转型是不合法的?

57

我相信我清楚地理解了在C++中上转换和下转换之间的一般区别。 我知道我们不能总是下转换,因为将基类指针转换为派生类指针将假定被指向的基类对象具有派生类的所有成员。

在这学期早期,我的教授告诉我们有时在C++中进行上转换也是不合法的,但是我在笔记中错过了原因,并且我记不得发生这种情况的时间。

在C++中什么时候上转换是不合法的?


1
我不确定,但也许与Class可以从多个类继承有关,因此“父类”不是唯一的? - Ivan Kuckir
1
@IvanKuckir:在类型转换中,您始终需要指定目标类型。因此,您可以从多个类继承并不重要。编译器只是检查目标类型是否属于基类之一。问题在于,当您通过两条不同的路径间接继承相同类型时。 - MSalters
3个回答

49
如果你所说的“非法”是指不符合语法规则,那么当基类无法访问或存在二义性时,它会被视为非法。
  • 当基类为私有时,就会无法访问。

    class A {};
    class B : A {};
    ...
    B b;
    A *pa = &b; // ERROR: base class is inaccessible
    

    请注意,即使在C++11中,C风格的转换也可以“突破”访问保护,执行正式正确的向上转型。

    A *pa = (A *) &b; // OK, not a `reinterpret_cast`, but a valid upcast
    

    当然,应该避免使用这种用法。

  • 如果您的源类型包含目标类型的多个基础子对象(通过多重继承),则会产生歧义。

  • class A {};
    class B : public A {};
    class C : public A {};
    class D : public B, public C {};
    
    D d;
    A *pa = &d; // ERROR: base class is ambiguous
    
    在这种情况下,可以通过显式地“沿着”所需的向上转型路径进行中间向上转型来执行向上转型,直到基类不再模糊为止。

    在这种情况下,可以通过显式地“沿着”所需的向上转型路径进行中间向上转型来执行向上转型,直到基类不再模糊为止。

    B* pb = &d;
    A* pa = pb; // OK: points to 'D::B::A' subobject
    

6
为什么这应该“不惜任何代价避免”,为什么这是“当然的”? - Lightness Races in Orbit
8
@Lightness Races in Orbit:因为这是一种违反语言非常基本原则的hack。当基类不可访问时,语言的作者们面临着一个艰难的选择:要么将其设为reinterpret_cast(这可能会导致潜在且几乎无法检测到的错误,如果基类访问发生变化),要么将其设为破坏保护的static_cast(他们选择了后者)。这两种变体都很糟糕。 - AnT stands with Russia
4
在我看来,“不惜一切代价”有些过于夸张了。确实,应该作为一项规则来避免这种情况的发生,但是如果必须做出选择,可能会有更为严重的后果,因此某些代价是可以接受的。 - Jerry Coffin
2
@Lightness Races in Orbit: 我不知道的是,为什么他们没有在这种情况下使其格式不正确。显然,他们想保留C风格转换的遗留属性,这对于指针转换“总是有效”。 - AnT stands with Russia
4
@AndreyT:在最近的两个标准中,引入了某些特性和构造,它们违背了这门语言非常基本的原则。除了极其可怕的黑客之外,我不相信“不惜一切代价避免”的论点。无论如何,我评论的重点是你在回答中没有写出任何理由支持你的论断;也许现在你可以这样做? - Lightness Races in Orbit
显示剩余4条评论

17

如果基类存在二义性(通过不同的路径继承了两次或更多次),那么你不能在单一步骤中进行向上转换。

如果基类是不可访问的,则仅能使用C样式转换进行向上转换。这是该转换的一个特殊情况,它是唯一可以胜任此项工作的转换类型。实质上,它的行为类似于未受可访问性限制的static_cast


C++11 §5.4/4标准:

… 对于以下情况,在[a C cast]执行static_cast时即使基类是不可访问的,转换也是有效的:

  • 派生类类型的对象指针或派生类类型的lvalue或rvalue可以分别显式转换为非二义性的基类类型的指针或引用;
  • 派生类类型的成员指针可以显式转换为非虚拟基类类型的成员指针;
  • 非二义性非虚拟基类类型的对象指针、非二义性非虚拟基类类型的glvalue或非二义性非虚拟基类类型的成员指针可以分别显式转换为派生类类型的指针、引用或成员指针。

二义性示例:

struct Base {};
struct M1: Base {};
struct M2: Base {};
struct Derived: M1, M2 {};

auto main() -> int
{
    Derived d;
    //static_cast<Base&>( d );                      //! Ambiguous
    static_cast<Base&>( static_cast<M2&>( d ) );    // OK
}

无法访问的基础示例,通常需要在转换中进行地址调整:

struct Base { int value; Base( int x ): value( x ) {} };

class Derived
    : private Base
{
public:
    virtual ~Derived() {}       // Just to involve an address adjustment.
    Derived(): Base( 42 ) {}
};

#include <iostream>
using namespace std;

auto main() -> int
{
    Derived d;
    Base& b = (Base&) d;
    cout << "Derived at " << &d << ", base at " << &b << endl;
    cout << b.value << endl;
};

2
特别是对于第二个例子。虽然我会删除“本质上”,因为您给出了无法访问的基类转换的确切定义。 - Deduplicator
1
@Deduplicator:感谢您的评论。 "本质上"是一个回避问题的词,它涵盖了两种情况。(1) cast符号(C cast)可以添加const_cast。(2) 如果指定类型既不是基类也不是派生类或同一类,则会愉快地执行reinterpret_cast(可能还包括添加的const_cast),这是通往灾难的一步。不确定是否应该包含这些细节。 - Cheers and hth. - Alf
1
离题了,但是你为什么写 auto main() -> int 而不是只写 int main() 呢?我不明白为什么在返回类型已经明确的情况下使用自动类型推断语法(而且不依赖于任何函数参数的 decltype)。只是想理解一下是否有一些微妙的见解你拥有而我没有注意到的。 - Cornstalks
1
@Cornstalks:我看不出为什么要在使用auto语法的同时使用C++11之前的函数声明语法。无论如何,都必须使用auto语法。除了(1)成为函数声明的单一通用语法外,它还(2)允许您在每次查看代码时在同一位置看到函数名称,并且(3)在返回类型规范中使用未限定类型,适用于成员函数。尽管如此,我仍然将main作为特例,在同一行上编写返回类型,并使用旧语法声明void函数,类似于Pascal的procedure - Cheers and hth. - Alf

10

在C++中,向上转型存在两种情况会被视为不合法(在编译时会被诊断):

  1. 涉及的基类不可访问

    class base {};
    class derived : base {};
    
    int main() {
        derived x;
        base& y = x; // invalid because not accessible.
        // Solution: C-style cast (as static_cast without access-check)
        base& y1 = (base&)x;
    }
    
  2. 问题中的基类子对象不是明确的

  3. class base {};
    struct A1 : base {};
    struct A2 : base {};
    
    struct derived : A1, A2 {};
    int main() {
        derived x;
        base& y = x; // invalid because ambiguous.
        // Solution 1, scope resolution:
        base& y1 = static_cast<A1::base&>(x);
        base& y2 = static_cast<A2::base&>(x);
        // Solution 2, intermediate unambiguous steps:
        A1& a1 = x;
        A2& a2 = x;
        base& ya1 = a1;
        base& ya2 = a2;
    }
    

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接