C++ virtual function not called in subclass
Asked Answered
P

8

7

Consider this simple situation:

A.h

class A {
public:
    virtual void a() = 0;
};

B.h

#include <iostream>

class B {
public:
    virtual void b() {std::cout << "b()." << std::endl;};
};

C.h

#include "A.h"
#include "B.h"

class C : public B, public A {
public:
    void a() {std::cout << "a() in C." << std::endl;};
};

int main() {
    B* b = new C();
    ((A*) b)->a();    // Output: b().

    A* a = new C();
    a->a();           // Output:: a() in C.

   return 0;
}

In other words:
- A is a pure virtual class.
- B is a class with no super class and one non-pure virtual function.
- C is a subclass of A and B and overrides A's pure virtual function.

What surprises me is the first output i.e.

((A*) b)->a();    // Output: b().

Although I call a() in the code, b() is invoked. My guess is that it is related to the fact that the variable b is a pointer to class B which is not a subclass of class A. But still the runtime type is a pointer to a C instance.

What is the exact C++ rule to explain this, from a Java point of view, weird behaviour?

Printery answered 20/1, 2010 at 21:52 Comment(2)
Put simply: B's are not A's! They are totally unrelated to each other, but your (not good to use!) C-style cast doesn't care either way. dynamic_cast will correctly traverse your hierarchy. When you cast unrelated pointer types, you get undefined behavior. That means anything could happen, from it seeming to work to blowing your computer up.Dot
Don't forget the nasal demons.Otten
B
24

You are unconditionally casting b to an A* using a C-style cast. The Compiler doesn't stop you from doing this; you said it's an A* so it's an A*. So it treats the memory it points to like an instance of A. Since a() is the first method listed in A's vtable and b() is the first method listed in B's vtable, when you call a() on an object that is really a B, you get b().

You're getting lucky that the object layout is similar. This is not guaranteed to be the case.

First, you shouldn't use C-style casts. You should use C++ casting operators which have more safety (though you can still shoot yourself in the foot, so read the docs carefully).

Second, you shouldn't rely on this sort of behavior, unless you use dynamic_cast<>.

Brotherly answered 20/1, 2010 at 21:59 Comment(2)
You can completely rely on cross-casting if dynamic_cast is used. It's 100% guaranteed to work or your money back.Brno
@coppro: if you use dynamic_cast, yes. I was trying to emphasize not to rely on this working with the C-style cast. I updated my last statement to make that more clear.Brotherly
C
11

Don't use a C-style cast when casting across a multiple inheritance tree. If you use dynamic_cast instead you get the expected result:

B* b = new C();
dynamic_cast<A*>(b)->a();
Carruthers answered 20/1, 2010 at 22:0 Comment(10)
This should be a runtime error within the example given. Correct?Bergman
@tehMick, no it will be undefined behaviorIsaisaac
@teh @Trent: Both of you are wrong. :P It works correctly, traversing the inheritance hierarchy.Dot
I thought dynamic_cast was designed explicitly to provide run-time type checking. If it doesn't catch this, what good is it?Bergman
It does runtime type checking. And it sees that b is an instance of C, which does inherit from A.Otten
@jeffamaphone: Right, which should result in a runtime error once the call to a() on a null object is attempted.Bergman
Note that dynamic_cast<> only works if you have run-time type information enabled (/GR on the Windows compiler).Brotherly
Oh, I see, sorry - it's an object of type C, not B. So, it should work. My mistake.Bergman
@tehMick, dynamic_cast knows that the object type is C. Since C derives from A, it is able to make the cast without run-time type checking finding anything to complain about.Chagall
@GMan, I was saying that the original code was undefined behavior, I mis-interpreted tehMick's comment.Isaisaac
C
5

You are starting with a B* and casting it to A*. Since the two are unrelated, you're delving into the sphere of undefined behavior.

Chagall answered 20/1, 2010 at 22:0 Comment(0)
B
2

((A*) b) is an explicit c-style cast, which is allowed no matter what the types pointed to are. However, if you try to dereference this pointer, it will be either a runtime error or unpredictable behavior. This is an instance of the latter. The output you observed is by no means safe or guaranteed.

Bergman answered 20/1, 2010 at 22:0 Comment(0)
V
2

A and B are no related to each other by means of inheritance, which means that a pointer to B cannot be transformed into a pointer to A by means of either upcast or downcast.

Since A and B are two different bases of C, what you are trying to do here is called a cross-cast. The only cast in C++ language that can perform a cross-cast is dynamic_cast. This is what you have to use in this case in case you really need it (do you?)

B* b = new C(); 
A* a = dynamic_cast<A*>(b);
assert(a != NULL);
a->a();    
Vaulting answered 20/1, 2010 at 22:19 Comment(0)
W
1

The following line is a reinterpret_cast, which points at the same memory but "pretends" it is a different kind of object:

((A*) b)->a();

What you really want is a dynamic_cast, which checks what kind of object b really is and adjust what location in memory to point to:

dynamic_cast<A*>(b)->a()

As jeffamaphone mentioned, the similar layout of the two classes is what causes the wrong function to be called.

Wizard answered 20/1, 2010 at 22:4 Comment(0)
O
1

There is almost never an occasion in C++ where using a C-style cast (or its C++ equivalent reinterpret_cast<>) is justified or required. Whenever you find yourself tempted to use one of the two, suspect your code and/or your design.

Octosyllable answered 20/1, 2010 at 22:8 Comment(0)
S
0

I think you have a subtle bug in casting from B* to A*, and the behaviour is undefined. Avoid using C-style casts and prefer the C++ casts - in this case dynamic_cast. Due to the way your compiler has laid out the storage for the data types and vtable entries, you've ended up finding the address of a different function.

Sprint answered 20/1, 2010 at 22:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.