57

I am pretty sure I understand the general difference between upcasting and downcasting, particularly in C++. I understand that we can't always downcast because casting a base class pointer to a derived class pointer would assume that the base class object being pointed to has all the members of the derived class.

Early in the semester, my professor told the class that it is also sometimes illegal to upcast in C++, but I seem to have missed the reason why in my notes, and I can't remember when this occurs.

When is it illegal to upcast in C++?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Connor Foley
  • 568
  • 4
  • 9
  • 1
    I am not sure, but maybe it has something to do with fact, that Class can inherit from multiple classes, so "parent clas" is not unique? – Ivan Kuckir Nov 17 '14 at 21:55
  • 1
    @IvanKuckir: In a cast, you always specify the destination type. So it doesn't matter that you can inherit from multiple classes. The compile just checks if the destination type is among the base classes. What **is** a problem is when you indirectly inherit from the same type via two different paths. – MSalters Nov 18 '14 at 09:37

3 Answers3

49

If by "illegal" you mean ill-formed, then it is illegal if the base class is inaccessible or ambiguous.

  • It is inaccessible when, for example, the base class is private.

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

    Note that even in C++11 a C-style cast can "break through" access protection and perform a formally correct upcast

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

    This usage should be avoided, of course.

  • It is ambiguous if your source type contains multiple base subobjects of the target type (through multiple inheritance).

    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
    

    In such cases the upcast can be performed by explicitly "walking" the desired upcast path with intermediate upcasts to the point where the base is no longer ambiguous

    B* pb = &d;
    A* pa = pb; // OK: points to 'D::B::A' subobject
    
AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • 6
    Why should it be "avoided at all costs", and why is this "of course"? – Lightness Races in Orbit Nov 17 '14 at 23:05
  • 8
    @Lightness Races in Orbit: Becuase it is a hack that defeats very fundamental principles of the language. When the base is inaccessible, the autors of the language were faced with a tough choice: either make it an `reinterpret_cast` (which would create a potential for subtle and virtually undetectable errors if base access changes) or make it protection-defeating `static_cast` (which is what they chose). Both variants are horrible. – AnT stands with Russia Nov 18 '14 at 00:10
  • 4
    IMO, "at all costs" is over-stating the situation. Yes, it should be avoided as a rule, but there are considerably worse, so if it comes down to a choice, some costs may be acceptable. – Jerry Coffin Nov 18 '14 at 01:19
  • 2
    @Lightness Races in Orbit: What I don't know is why they didn't make it ill-formed in cases like that. Apparently, they wanted to preserve the legacy properties of C-style cast, which "always works" for pointer conversions. – AnT stands with Russia Nov 18 '14 at 01:38
  • 4
    @AndreyT: There are features and constructs introduced into the two latest _standards_ that defeat the very fundamental principles of the language. I don't buy the "avoid at _all_ costs" argument for any but the very most horrific hacks. Anyway, the point of my comment was that you did not write any rationale alongside that assertion in your answer; perhaps you could do so now? – Lightness Races in Orbit Nov 18 '14 at 10:09
  • 1
    @AndreyT As both possibilities are bad, why couldn't they simply forbid this cast? – maaartinus Nov 18 '14 at 11:21
  • yeah, I'm with you both on this one: many post-99 additions, including C++11 and proposed later additions IMO make C++ an ugly version of Java/C#/Haskell mix, not a simple yet powerful language it once were; exactly why I switched to pure C for native code and Java for high-level abstraction... although lambdas in Java *does* make me itch. NB saying "just don't use the features you don't need in XXX" is not an answer for me - if something's lacking and most of the new features are useless for me, switching to lang giving better features is the way to go IMO. Reminds me of Project Coin, BTW. –  Nov 18 '14 at 13:19
  • @maaartinus: Considering that C-style cast generally covers the functionality of `reinterpret_cast`, i.e it can cast `SomeType *` to `SomeCompletelyUnrelatedType *`, forbidding the cast in this specific situation would look rather pointless. – AnT stands with Russia Nov 22 '14 at 02:14
  • @AndreyT I don't mind calling `*_cast` explicitly in this case, it's just that when you write that, you know what you're doing. Unlike with `(A*)`. – maaartinus Nov 22 '14 at 05:51
17

If the base class is ambiguous (inherited two or more times via different paths) then you can’t do an upcast in a single step.

If the base class is inaccessible then the only way to upcast is to use a C style cast. This is a special case of that cast, it's the only one that can do the job. Essentially it then behaves as a static_cast that's not limited by accessibility.


Standardese.

C++11 §5.4/4:

… in [a C cast] performing a static_cast in the following situations the conversion is valid even if the base class is inaccessible:

  • a pointer to an object of derived class type or an lvalue or rvalue of derived class type may be explicitly converted to a pointer or reference to an unambiguous base class type, respectively;
  • a pointer to member of derived class type may be explicitly converted to a pointer to member of an unambiguous non-virtual base class type;
  • a pointer to an object of an unambiguous non-virtual base class type, a glvalue of an unambiguous non-virtual base class type, or a pointer to member of an unambiguous non-virtual base class type may be explicitly converted to a pointer, a reference, or a pointer to member of a derived class type, respectively.

Example of ambiguity:

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
}

Example of inaccessible base, with (usually) address adjustment in the cast:

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;
};
Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • 2
    +1 Especially for the second example. Though I would remove essentially, as you give the exact definition of the cast for inaccessible bases. – Deduplicator Nov 17 '14 at 22:16
  • 1
    @Deduplicator: Thanks for that comment. The "essentially" is a weasel word, covering two cases. (1) The *cast* notation (C cast) can add a `const_cast`. (2) In the case where the specified type is not a base class and not a derived or same class, it will happily perform a `reinterpret_cast` (possibly also this with added `const_cast`), as a step in the direction of disaster. Not sure if I should include such details. – Cheers and hth. - Alf Nov 17 '14 at 22:21
  • 1
    Off topic, but why did you write `auto main() -> int` instead of just `int main()`? I don't understand why you'd use automatic type deduction syntax when the return type is just explicitly stated anyway (and doesn't rely on any `decltype` of the function parameters). Just trying to understand if there's some subtle insight you have that I'm missing. – Cornstalks Nov 17 '14 at 22:30
  • 1
    @Cornstalks: I see no reason to use the pre-C++11 function declaration syntax in addition to the `auto` syntax. The `auto` syntax has to be used anyway. In addition to (1) being a single general syntax for function declarations it (2) allows you to see the function name at the same place every time, for ease of scanning code, and (3) use unqualified types in return type spec, for a member function. Still I special-case `main`, writing the return type on the same line, and I declare `void` functions with the old syntax, similar to Pascal `procedure`. – Cheers and hth. - Alf Nov 17 '14 at 23:56
10

There are two cases in which upcasting is ill-formed in C++ (diagnosed at compile-time):

  1. The base-class in question is not accessible:

    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. The base-class sub-object in question is not unambiguous:

    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;
    }
    
Deduplicator
  • 44,692
  • 7
  • 66
  • 118