Изменение динамического типа объекта в C ++ - PullRequest
3 голосов
/ 16 июня 2019

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

Однако я слышал, что это не так от некоторых докладчиков на 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 дёшево и требует в основном из-за чистого интереса. Что отключает такие оптимизации компилятора?

1 Ответ

5 голосов
/ 16 июня 2019

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

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

EDIT: это не летать:

#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. Таким образом, он оптимизирует вызов виртуальной таблицы.

Этот код работает 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;
}

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...