0

I have a diamond inheritance structure in my code in which I have a pointer to the bottom object. I tried to case this to a pointer to the left of the two diamond sides, cast it again to the top of the diamond, and again to the right side. But apparently, C++ kind of remembers the order of casting and things don't work as expected. Example code:

#include <iostream>

class A
{
};

class B1 : public A
{
public:
    virtual int Return1() = 0;
};

class B2 : public A
{
public:
    virtual int Return2() = 0;
};

class C : public B1, public B2
{
public:
    virtual int Return1() { return 1; }
    virtual int Return2() { return 2; }
};

int main()
{
    C c;
    B1* b1 = &c;
    A* a = b1;
    B2* b2 = (B2*)a;
    std::cout << "Return2() = " << b2->Return2();
}

This results in Return2() = 1, so apparently this approach is wrong. I know that something like this works in C#, so my question would be: Is there a way in C++ to do what I'm attempting here or - if not - why is this not an option?

Dododude
  • 37
  • 5
  • 1
    How can something like this work in C# if there is no multi-inheritance there? – RoQuOTriX Jun 14 '21 at 08:25
  • @RoQuOTriX probably confusing interfaces and inheritance – Mansoor Jun 14 '21 at 08:26
  • You need to use `dynamic_cast` then it will work. Also B1, B2 must inherit A virtually + A must be polymorphic (just make destructor virtual). – ALX23z Jun 14 '21 at 08:28
  • Related/duplicate: [In C++, what is a virtual base class?](https://stackoverflow.com/questions/21558/in-c-what-is-a-virtual-base-class) – Yksisarvinen Jun 14 '21 at 08:28
  • @ALX23z -- there is no apparent requirement for virtual inheritance here. Sometimes you need to base objects of the same type. – Pete Becker Jun 14 '21 at 14:11
  • @PeteBecker he asked for diamond inheritance - which isn't the case in the example. But yeah, for `dynamic_cast` it isn't necessary. – ALX23z Jun 14 '21 at 14:49
  • @ALX23z -- no, the question says "I have a diamond structure"; people often say that when they see a repeated base. The question does not ask "how do I create a diamond structure". – Pete Becker Jun 14 '21 at 15:13
  • @PeteBecker I don't ever remember ever needing a repeated base. I always needed a shared base for the classes. I'd rather have the classes as member variables instead of inheriting from them. Thus I assume that he failed to implement diamond inheritance rather than meant to have multiple inheritance of A. – ALX23z Jun 14 '21 at 18:08
  • @ALX23z — yes, I figured that was why you said it. One thing I learned from twenty years on the C++ standards committee is that other people’s experience is vastly different from mine, and things that I thought were pointless were often important for others. Bjarne Stroustrup’s example for a repeated base is an intrusive list and a type that needs to belong to two lists. – Pete Becker Jun 14 '21 at 19:16

3 Answers3

1
  • As inheritance is not virtual (for A), you have "Y" inheritance (2 A),
A     A
|     |
B1    B2
 \   /
   C

not a diamond (1 A).

  • Avoid C-cast which might result in reinterpret_cast, and most reinterpret_cast usage leads to Undefined Behavior (UB).

  • You might use dynamic_cast in your case to have expected behavior (A need to be polymorphic for that, default virtual destructor does the job):

class A
{
public:
    virtual ~A() = default; // Added to allow dynamic_cast
};

class B1 : public A
{
public:
    virtual int Return1() = 0;
};

class B2 : public A
{
public:
    virtual int Return2() = 0;
};

class C : public B1, public B2
{
public:
    // override used for extra check from compiler.
    int Return1() override { return 1; }
    int Return2() override { return 2; }
};

int main()
{
    C c;
    B1* b1 = &c;
    A* a = b1;
    B2* b2 = dynamic_cast<B2*>(a); // C-cast replaced by dynamic_cast
    assert(b2 != nullptr);
    std::cout << "Return2() = " << b2->Return2();
}

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
0

You can see the desired result by changing the last casting to:

B2* b2 = (B2*)&c;

This is an issue related to upcasting and downcasting.

astrid_coder
  • 141
  • 1
  • 6
0

The issue here is that you are using a C cast (T) expr, which is 99% of the time a bad idea in C++.

C casts only exist in C++ due to the need of being retrocompatible with C, and can behave in unexpected ways.

From here:

When the C-style cast expression is encountered, the compiler attempts to interpret it as the following cast expressions, in this order:

a) const_cast<new_type>(expression);

b) static_cast<new_type>(expression), with extensions: pointer or reference to a derived class is additionally allowed to be cast to pointer or reference to unambiguous base class (and vice versa) even if the base class is inaccessible (that is, this cast ignores the private inheritance specifier). Same applies to casting pointer to member to pointer to member of unambiguous non-virtual base;

c) static_cast (with extensions) followed by const_cast;

d) reinterpret_cast<new_type>(expression);

e) reinterpret_cast followed by const_cast.

The first choice that satisfies the requirements of the respective cast operator is selected, even if it cannot be compiled (see example)

The correct type of cast when downcasting in C++ is dynamic_cast<T>(expr), which checks if the object of the expression can be cast to the derived type T before performing it. If you did that, you would have got a compile time or runtime error, instead of getting a wrong behaviour.

C-style casts never perform dynamic casts, so (B2*) in B2* b2 = (B2*)a becomes equivalent to reinterpret_cast<B2*> which is a type of cast that blindly coerces any pointer type to any other. In this way C++ can't do any of the required pointer "magic" it's usually needed to convert a C* into a valid B2*.

Given that polymorphism in C++ is implemented through virtual dispatching using method tables, and that the pointer in b2 doesn't point to the correct base class (given that it was actually a pointer to B1), you are accessing the vtable for B1 instead of B2 through b2.

Both Return1 and Return2 are the first functions in the vtables of their respective abstract classes, so in your case Return1 is mistakenly called - you could largely approximate virtual invocations with something like b2->vtable[0]() in most implementations. Given that neither of the two methods touch this, nothing breaks and the function returns without crashing the program (which is not guaranteed, given this whole thing is undefined behaviour).

mcilloni
  • 693
  • 5
  • 9