Изменение динамического типа объекта в C++

В следующем вопросе один из ответов предполагал, что динамический тип объекта не может измениться: Когда может измениться динамический тип упомянутого объекта?

Однако я слышал, что это неправда от какого-то докладчика на CPPCon или какой-то другой конференции.

И действительно, это не похоже на правду, потому что и GCC, и Clang перечитывают указатель vtable на каждой итерации цикла следующего примера:

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

Однако, если добавить следующее:

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;
}

затем функция g упрощается до return 10;, что действительно ожидается из-за final. Это также предполагает, что единственное возможное место, где динамика может измениться, находится внутри GetVal.

Я понимаю, что перечитывать указатель vtable дешево и спрашивать в основном из чистого интереса. Что отключает такие оптимизации компилятора?


person Nikita Petrenko    schedule 16.06.2019    source источник
comment
Объекты в C++ не имеют динамического типа — их тип фиксируется во время компиляции. Вы можете ссылаться на объекты с помощью указателей или ссылок, как если бы они были разных типов, но фактический тип объекта не изменился.   -  person    schedule 16.06.2019
comment
@NeilButterworth Если какое-то выражение glvalue относится к полиморфному объекту, тип его наиболее производного объекта называется динамическим типом. en.cppreference.com/w/cpp/language/type   -  person Nikita Petrenko    schedule 17.06.2019
comment
Потому что доступ осуществляется через указатель.   -  person    schedule 17.06.2019
comment
I've heard that it is not true from some speaker on CPPCon было бы полезно добавить ссылку на соответствующий разговор, потому что тогда можно было бы сказать вам, что имел в виду человек.   -  person t.niese    schedule 17.06.2019
comment
Относительно вышеупомянутого обмена комментариями; Стандарт здесь небрежен с терминологией. В разделе «Определения» динамический тип определяется только для значений gl, а не для объектов. Однако Стандарт использует неопределенный термин динамический тип объекта в нескольких местах. Некоторые из этих вариантов использования могут означать динамический тип glvalue, однако некоторые не могут. Возможно, в остальных случаях они означают наиболее производный объект, подобъектом которого является объект.   -  person M.M    schedule 17.06.2019
comment
Похоже, он не оптимизирует const, так что...   -  person curiousguy    schedule 17.06.2019


Ответы (1)


Вы не можете изменить тип объекта. Вы можете уничтожить объект и создать что-то новое в той же памяти - это максимально близко к "изменению" типа объекта. По этой же причине компилятор некоторого кода фактически перечитывает vtable. Но проверьте это https://godbolt.org/z/Hmq_5Y - vtable читается только один раз. В общем - не может изменить тип, но может разрушать и создавать из пепла.

Отказ от ответственности: пожалуйста, пожалуйста, не делайте ничего подобного. Это ужасная идея, запутанная, трудная для понимания, компилятор может понять ее немного по-другому, и все пойдет наперекосяк. Если вы задаете такие вопросы, вы, конечно, не хотите применять их на практике. Задайте свою настоящую проблему, и мы ее исправим.

РЕДАКТИРОВАТЬ: это не летать:

#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;
}

Почему? Потому что в основном компилятор видит B как конечный и компилятор по правилу языка знает, что он контролирует время жизни объекта b. Таким образом, он оптимизирует вызов виртуальной таблицы.

Этот код работает, хотя:

#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;
}

Я использовал new для выделения в куче, а не в стеке. Это не позволяет компилятору взять на себя управление временем жизни b. Что, в свою очередь, означает, что он больше не может предполагать, что содержимое b может не измениться. Обратите внимание, что попытка воскресить из пепла в методе GetVal тоже может не сработать - объект this должен жить не меньше, чем вызов GetVal. Что из этого сделает компилятор? Твоя догадка так же хороша как и моя.

В общем, если вы пишете код, который оставляет сомнения в том, как компилятор его интерпретирует (другими словами, вы попадаете в «серую зону», которая может быть понята по-разному вами, производителями компилятора, авторами языка и самим компилятором), вы напрашиваетесь на неприятности. . Пожалуйста, не делай этого. Спросите нас, зачем вам нужна подобная функция, и мы подскажем, как реализовать ее в соответствии с правилами языка или как обойти ее отсутствие.

person Radosław Cybulski    schedule 16.06.2019
comment
Не могли бы вы уточнить? Например, следующее не работает: godbolt.org/z/-1AMpY - person Nikita Petrenko; 17.06.2019
comment
Я почти уверен, что образец ваших работ не определен. Мало того, что B* указывает на объект, который не является B (даже если вы используете его только как A*, это не делает его приемлемым), вы также предполагаете, что преобразования полиморфных указателей всегда тривиальны (что не является случай с множественным или виртуальным наследованием). - person kmdreko; 17.06.2019