3

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?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Nikita Petrenko
  • 1,068
  • 1
  • 7
  • 10
  • 3
    Objects in C++ don't have a "dynamic type" - their type is fixed at compile-time. You can refer to objects via pointers or references as if they were of different types, but the actual type of the object is unchanged. –  Jun 16 '19 at 20:56
  • 4
    @NeilButterworth "If some glvalue expression refers to a polymorphic object, the type of its most derived object is known as the dynamic type." https://en.cppreference.com/w/cpp/language/type – Nikita Petrenko Jun 16 '19 at 21:01
  • Because the access is via a pointer. –  Jun 16 '19 at 21:04
  • `I've heard that it is not true from some speaker on CPPCon` it would be helpful to add a link to the corresponding talk because then it would be possible to tell you what the person meant. – t.niese Jun 17 '19 at 05:38
  • 3
    Regarding the above comment exchange; the Standard is sloppy with terminology here. In the Definitions section, "dynamic type" is only defined for glvalues, not objects. However the Standard does use the non-defined term "dynamic type of the object" in several places. Some of those usages could taken to mean "dynamic type of the glvalue", however some can't. Perhaps they mean "most-derived object of which the object is a subobject" in the other cases. – M.M Jun 17 '19 at 06:30
  • It doesn't seem to [optimize const](https://godbolt.org/z/X_lsyn) either so... – curiousguy Jun 17 '19 at 09:54

1 Answers1

5

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.

Radosław Cybulski
  • 2,952
  • 10
  • 21
  • Could you elaborate? E.g., the following does not work: https://godbolt.org/z/-1AMpY – Nikita Petrenko Jun 16 '19 at 21:14
  • 1
    I'm pretty sure your "works tho" sample is undefined. Not only that a `B*` points to an object that isn't a `B` (even though you only use it as an `A*` doesn't make it ok), you're also assuming that polymorphic pointer conversions are always trivial (which is not the case with multiple or virtual inheritance). – kmdreko Jun 17 '19 at 07:46