0

I was trying to figure out with what signatures C++ actually calls methods virtually, instead of using the base class. So I wrote the following:

#include <iostream>
#include <string>

using namespace std;
#define GREETER(type, op)\
    string greet(type t) {\
        return string(#type) + t op greet();\
    }

struct Unvirtual {
    string greet() {
        return "Unvirtual";
    }
};

struct UnvirtualUnoverride : Unvirtual {
    string greet() {
        return "UnvirtualUnoverride";
    }
};

struct Virtual {
    virtual string greet() {
        return "Virtual";
    }
};

struct VirtualUnoverride : Virtual {
    virtual string greet() {
        return "VirtualUnoverride";
    }
};

struct VirtualOverride : Virtual {
    virtual string greet() override {
        return "VirtualOverride";
    }
};

GREETER(Unvirtual, .)
//GREETER(Unvirtual&, .)
GREETER(Unvirtual*, ->)

// GREETER(UnvirtualUnoverride, .)
// GREETER(UnvirtualUnoverride&, .)
// GREETER(UnvirtualUnoverride*, ->)

GREETER(Virtual, .)
//GREETER(Virtual&, .)
GREETER(Virtual*, ->)

// GREETER(VirtualUnoverride, .)
// GREETER(VirtualUnoverride&, .)
// GREETER(VirtualUnoverride*, ->)

// GREETER(VirtualOverride, .)
// GREETER(VirtualOverride&, .)
// GREETER(VirtualOverride*, ->)

#define DEBUG(expr)\
    cout << #expr << endl;\
    cout << "\t" << expr << endl;

int main() {
    Unvirtual uv, &uvr = uv, *uvp = &uv;
    UnvirtualUnoverride uvu, &uvur = uvu, *uvup = &uvu;
    Virtual v, &vr = v, *vp = &v;
    VirtualUnoverride vu, &vur = vu, *vup = &vu;
    VirtualOverride vo, &vor = vo, *vop = &vo;

    DEBUG(greet(uv));
    DEBUG(greet(uvr));
    DEBUG(greet(uvp));

    DEBUG(greet(uvu));
    DEBUG(greet(uvur));
    DEBUG(greet(uvup));

    DEBUG(greet(v));
    DEBUG(greet(vr));
    DEBUG(greet(vp));

    DEBUG(greet(vu));
    DEBUG(greet(vur));
    DEBUG(greet(vup));

    DEBUG(greet(vo));
    DEBUG(greet(vor));
    DEBUG(greet(vop));

    system("pause");
    return 0;
}

The program's output was:

greet(uv)
    UnvirtualUnvirtual
greet(uvr)
    UnvirtualUnvirtual
greet(uvp)
    Unvirtual*Unvirtual
greet(uvu)
    UnvirtualUnvirtual
greet(uvur)
    UnvirtualUnvirtual
greet(uvup)
    Unvirtual*Unvirtual
greet(v)
    VirtualVirtual
greet(vr)
    VirtualVirtual
greet(vp)
    Virtual*Virtual
greet(vu)
    VirtualVirtual
greet(vur)
    VirtualVirtual
greet(vup)
    Virtual*VirtualUnoverride
greet(vo)
    VirtualVirtual
greet(vor)
    VirtualVirtual
greet(vop)
    Virtual*VirtualOverride
Press any key to continue . . .

From this I conclude that C++ only dispatches method m on object o dynamically when: m is declared virtual and o is a pointer.

Are these conclusions accurate? Have I missed any tests?

Matt G
  • 1,661
  • 19
  • 32
  • 4
    When you pass a `VirtualUnoverride` (or `VirtualOverride`) to a function taking a `Virtual` by value, it's sliced. – T.C. Jan 27 '15 at 00:27

2 Answers2

2

First, the answer to the underlying question is very simple: C++ implements dynamic dispatching via virtual functions.

To go over your examples:

Unvirtual and UnvirtualUnoverride are a case of shadowing, where UnvirtualUnoverride::greet shadows Unvirtual::greet and thus is chosen if the type of the object it is being called on is UnvirtualUnoverride. As you have correctly concluded from your tests, the dispatch is not dynamic at all.

VirtualUnoverride and VirtualOverride do not actually do anything different from one another w.r.t. greet. The reason for this is that override is only an assistance to the programmer that is intended to ensure that a base function is being overriden instead of introducing a new function. For example, if Virtual::greet was changed to string greet() const, the override keyword would make your day: VirtualOverride will stop compilation with an error - VirtualUnoverride will silently add another overload (not override) to that function.

The combination of the previous two paragraphs also explain why you do not have a UnvirtualOverride: It is impossible to override a non-virtual function and thus compilation will be aborted.

To explain why it seems to you as if the object on which the function is called must be a pointer, we have to delve a tiny bit deeper into how your test framework actually works:

The previous should have explained all test cases for the Unvirtual and UnvirtualUnoverride cases. The Virtual cases are also trivial, since all objects, pointers and references only refer to the base class. Since UnvirtualUnoverride and UnvirtualOverride are basically the same, let us just go over one of them:

VirtualOverride vo, &vor = vo, *vop = &vo;
DEBUG(greet(vo));
DEBUG(greet(vor));
DEBUG(greet(vop));

To understand what really happens, consider that only four of your greet(T) functions exist of which only two could be applicable:

GREETER(Unvirtual, .) // obviously not applicable
GREETER(Unvirtual*, ->) // obviously not applicable
GREETER(Virtual, .)
GREETER(Virtual*, ->)

Now, to resolve the relevant macros:

string greet(Virtual t) {
    return string("Virtual") + t.greet();
}

string greet(Virtual* t) {
    return string("Virtual*") + t->greet();
}

The pointer version will perform a standard pointer to base type conversion and thus behave as expected.

The other version, however, will make a copy of your argument. This means, that you now have a Virtual object that has been constructed from a VirtualOverride object. Since no data members exist, this construction is boring to say the least, but it also means that your actual dispatch happens on an object that has the static and dynamic type of Virtual - no matter which of the objects you pass to the function!

This problem is called object slicing and can be very confusing to people not intimately familiar with C++. In general, you should always ensure that you do not pass polymorphic objects by reference. The easiest way would be to make the Virtual::greet function pure virtual, thus transforming Virtual into an abstract class.

Community
  • 1
  • 1
danielschemmel
  • 10,885
  • 1
  • 36
  • 58
1

Dynamic dispatch occurs regardless of whether you're using a pointer or a reference. references too. In fact, don't forget that unless operator-> is overloaded, o->m() is identical to (*o).m(). Therefore if dynamic dispatch occurs in one case, it must occur in the other.

As far as I know, dynamic dispatch when calling a virtual function is only disabled in the following two cases:

  1. You explicitly qualify the function to be called using the class in which it is defined. o->Base::m() is guaranteed to dispatch statically and call Base::m and not Derived::m, as opposed to o->m() which dispatches dynamically.
  2. An object is currently in the process of construction or destruction and the virtual function is called on that object itself (not another object of the same class, or an object of a different class). In this case the function that will be called is the final overrider in the class to which the constructor or destructor belongs, not the final overrider in the class of the complete object under construction or destruction.

These are the only exceptions I'm aware of, but there are also cases where dynamic dispatch occurs but you might not realize it. There's the slicing problem, as pointed out by others. There are also cases in which the function in the derived class has the same name as one in the base class, but different parameter types; in this case, the former doesn't overload the latter, so dynamic dispatch on that function is trivial.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312