Nikita Petrenko
Nikita Petrenko

Reputation: 1088

Changing dynamic type of an object in C++

In the following question one of the answers suggested that the dynamic type of an object cannot change: When may the dynamic type of a referred to object change?

However, I've heard that it is not true from some speaker on CPPCon or some other conference.

And indeed it does not seem to be true, because both GCC and Clang re-read vtable pointer on every loop iteration of the following example:

class A {
public:
    virtual int GetVal() const = 0;
};

int f(const A& a){
    int sum = 0;
    for (int i = 0; i < 10; ++i) {
        // re-reads vtable pointer for every new call to GetVal
        sum += a.GetVal();
    }
    return sum;
}

https://godbolt.org/z/MA1v8I

However, if one adds the following:

class B final : public A {
public:
    int GetVal() const override {
        return 1;
    }
};

int g(const B& b){
    int sum = 0;
    for (int i = 0; i < 10; ++i) {
        sum += b.GetVal();
    }
    return sum;
}

then function g is simplified to return 10;, which is indeed expected because of final. It also suggests that the only possible place where the dynamic may change is inside GetVal.

I understand that re-reading vtable pointer is cheap, and asking mostly because of pure interest. What disables such compiler optimizations?

Upvotes: 3

Views: 974

Answers (1)

Radosław Cybulski
Radosław Cybulski

Reputation: 2992

You can't change type of object. You can destroy object and create something new in the same memory - this is as closest as you can get to "changing" object type. This is also why for some code compiler will actually reread vtable. But check this one https://godbolt.org/z/Hmq_5Y - vtable is read only once. In general - can't change type, but can destroy and create from ashes.

Disclaimer: please, please, don't do anything like that. This is terrible idea, messy, hard to understand by anyone, compiler might understand it slightly differently and everything will pretty much go south. If you ask that kind of question, you certainly don't want to implement them in practise. Ask your real problem and we will fix it.

EDIT: this ain't fly:

#include <iostream>

class A {
public:
    virtual int GetVal() const = 0;
};

class C final : public A {
public:
    int GetVal() const override {
        return 0;
    }
};

class B final : public A {
public:
    int GetVal() const override {
        const void* cptr = static_cast<const void*>(this);
        this->~B();
        void* ptr = const_cast<void*>(cptr);
        new (ptr) C();
        return 1;
    }
};

int main () {
    B b;
    int sum = 0;
    for (int i = 0; i < 10; ++i) {
        sum += b.GetVal();
    }
    std::cout << sum << "\n";
    return 0;
}

Why? Because in main compiler sees B as final and compiler by language rule knows, that it controls lifetime of object b. So it optimizes virtual table call.

This code works tho:

#include <iostream>

class A {
public:
    virtual ~A() = default;
    virtual int GetVal() const = 0;
};

class C final : public A {
public:
    int GetVal() const override {
        return 0;
    }
};

class B final : public A {
public:
    int GetVal() const override {
        return 1;
    }
};

static void call(A *q, bool change) {
    if (change) {
        q->~A();
        new (q) C();
    }
    std::cout << q->GetVal() << "\n";
}
int main () {
    B *b = new B();
    for (int i = 0; i < 10; ++i) {
        call(b, i == 5);
    }
    return 0;
}

I've used new to allocate on heap, not on stack. This prevents compiler from assuming lifetime management of b. Which in turn means it no longer can assume content of b might not change. Note, that trying to do raising from ashes in GetVal method might not go well also - this object must live as at least long as call to GetVal. What will compiler make of it? Your guess is as good as mine.

In general, if you write code, which leaves any doubt how compiler will interprete it (in other words you enter "grey area", which might be understood differently by you, compiler makes, language writers and compiler itself), you ask for troubles. Please, don't do that. Ask us, why you need feature like this and we will tell you, how either make it happen according to the language rules or how you can work around lack of it.

Upvotes: 5

Related Questions