Разрешено ли объекту законно изменять свой тип во время своего существования в C++?

У меня есть этот код:

class Class {
public:
    virtual void first() {};
    virtual void second() {};
};

Class* object = new Class();
object->first();
object->second();
delete object;

что я компилирую с помощью Visual C++ 10 с /O2 и имею эту дизассемблирование:

282:    Class* object = new Class();
00403953  push        4  
00403955  call        dword ptr [__imp_operator new (4050BCh)]  
0040395B  add         esp,4  
0040395E  test        eax,eax  
00403960  je          wmain+1Ch (40396Ch)  
00403962  mov         dword ptr [eax],offset Class::`vftable' (4056A4h)  
00403968  mov         esi,eax  
0040396A  jmp         wmain+1Eh (40396Eh)  
0040396C  xor         esi,esi  
283:    object->first();
0040396E  mov         eax,dword ptr [esi]  
00403970  mov         edx,dword ptr [eax]  
00403972  mov         ecx,esi  
00403974  call        edx  
284:    object->second();
00403976  mov         eax,dword ptr [esi]  
00403978  mov         edx,dword ptr [eax+4]  
0040397B  mov         ecx,esi  
0040397D  call        edx  
285:    delete object;
0040397F  push        esi  
00403980  call        dword ptr [__imp_operator delete (405138h)]  

Обратите внимание, что в 00403968 адрес начала объекта (где хранится vptr) копируется в регистр esi. Затем в 0040396E этот адрес используется для получения vptr, а значение vptr используется для получения адреса first(). Затем в 00403976 снова извлекается vptr и используется для получения адреса second().

Почему vptr извлекается дважды? Возможно ли, что объект vptr изменился между вызовами, или это просто недооптимизация?


person sharptooth    schedule 18.09.2012    source источник
comment
Странный. Я думал, что все должно быть без операции. Вероятно, это недооптимизация, если бы он заглянул внутрь функций, то мог бы увидеть, что ни одна из них не меняет esi.   -  person Luchian Grigore    schedule 18.09.2012


Ответы (3)


Почему vptr извлекается дважды? Может ли объект изменить свой vptr между вызовами или это просто недооптимизация?

Учитывать:

object->first();

Этот вызов может уничтожить объект и создать новый в том же фрагменте памяти. Следовательно, после этого вызова нельзя делать никаких предположений о состоянии. Например.:

#include <new>

struct Class {
    virtual void first();
    virtual void second() {}
    virtual ~Class() {}
};

struct OtherClass : Class {
    void first() {}
    void second() {}
};

void Class::first() {
    void* p = this;
    static_assert(sizeof(Class) == sizeof(OtherClass), "Oops");
    this->~Class();
    new (p) OtherClass;
}

int main() {
    Class* object = new Class();
    object->first();
    object->second();
    delete object;
}

Компиляторы могут оптимизировать ненужную загрузку регистров, если эта функция является встроенной и/или используется генерация кода во время компоновки.


Как отметили DeadMG и Стив Джессоп, приведенный выше код демонстрирует неопределенное поведение. Согласно 3.8/7 стандарта С++ 2003:

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

  • хранилище для нового объекта точно перекрывает место хранения, которое занимал исходный объект, и
  • новый объект имеет тот же тип, что и исходный объект (игнорируя cv-квалификаторы верхнего уровня), и
  • тип исходного объекта не является константным, и, если тип класса, не содержит нестатических элементов данных, чей тип является константным или ссылочным типом, и
  • исходный объект был наиболее производным объектом (1.8) типа T, а новый объект является наиболее производным объектом типа T (то есть они не являются подобъектами базового класса).

Приведенный выше код не удовлетворяет требованию 2 из приведенного выше списка.

person Maxim Egorushkin    schedule 18.09.2012
comment
@LuchianGrigore: Возможно, LTCG не помогает пока, но это оптимизация, доступная для потенциальных будущих компиляторов. - person j_random_hacker; 18.09.2012
comment
@LuchianGrigore: gcc-4.7.1 оптимизирует исходный код, опубликованный в вызове operator new(), за которым следует вызов operator delete() с -O3. Хотите ли вы предоставить какие-либо подтверждающие доказательства вашего утверждения? - person Maxim Egorushkin; 18.09.2012
comment
@MaximYegorushkin, вы использовали gcc с визуальным C++? Потому что иначе вы не отвечаете на вопрос. Речь идет о конкретном компиляторе с определенной настройкой (-O2). А с визуальным C++ -O2 с генерацией кода во время компоновки дает тот же результат. - person Luchian Grigore; 18.09.2012
comment
Не совсем. Создание чего-либо, кроме еще одного Class в этом пространстве, было бы гигантской кучей UB. Компилятору не нужно беспокоиться об этом потенциальном сценарии. - person Puppy; 20.09.2012
comment
@DeadMG: Создание чего-либо, кроме другого класса в этом пространстве, было бы гигантской кучей UB. Ну, не совсем так. Не хотите ли вы предоставить какие-либо подтверждающие доказательства этого смелого утверждения? - person Maxim Egorushkin; 20.09.2012
comment
@Maxim: object->second() это UB. Указатель object некорректно ссылается ни на какой объект, и уж точно не на вновь созданный объект типа OtherClass. См. 3.8/7 в стандарте. Ваш код нарушает второй пункт маркера (новый объект относится к тому же времени, что и исходный объект) или четвертый (они не являются подобъектами базового класса), в зависимости от того, является ли это полным объектом или его Class базой, на которую, по вашему мнению, object ссылается к. На самом деле он не относится ни к одному из них, хотя на практике, конечно, содержит тот же адрес. Так что вы не увидите проблемы, если оптимизация вас не подведет. - person Steve Jessop; 20.09.2012
comment
@SteveJessop: Объект-указатель неправильно ссылается на какой-либо объект, и уж точно не на недавно созданный объект типа OtherClass. Я не понимаю, почему он не может правильно ссылаться на объект. После вызова указателя object->first() object корректно ссылается на только что созданный объект OtherClass, не так ли? - person Maxim Egorushkin; 20.09.2012
comment
@Максим: нет, это не так. 3.8/7 конкретно указано, что он относится к новому объекту, если условия соблюдены. Условия не выполняются. В стандарте больше нигде не сказано, что он относится к новому объекту (если что-то было, то какой смысл в 3.8/7 тщательно определять эти условия, когда указатель ссылается на новый объект, независимо от того, выполняются ли они или нет?). Таким образом, это не относится к новому объекту. Раньше он ссылался на старый объект, но вы уничтожили старый объект. - person Steve Jessop; 20.09.2012
comment
То есть, что касается стандарта. Возможно, MSVC взял на себя в качестве расширения возможность указателя ссылаться на новый объект, и поэтому vptr перезагружается. Но есть и другие причины, по которым vptr мог быть перезагружен, поэтому из этого кода мы не можем сделать вывод, что MS гарантирует, что это будет происходить всегда. - person Steve Jessop; 20.09.2012
comment
@SteveJessop: я взглянул на 3.8/7, и действительно требуется, чтобы новый объект был точно такого же типа, чтобы указатель оставался действительным. Немного удивительно, что он не позволяет создать производный класс того же размера. Может быть, это позволяет компилятору избежать перезагрузки указателя vtable после каждого виртуального вызова (оптимизация, которую MSVC здесь не делает). - person Maxim Egorushkin; 20.09.2012

Он хранится в esi для сохранения между вызовами различных функций.

Конвенция Microsoft гласит

Компилятор генерирует код пролога и эпилога для сохранения и восстановления регистров ESI, EDI, EBX и EBP, если они используются в функции.

поэтому указатель, хранящийся в esi, останется, а указатель this в ecx может и не измениться.

person Bo Persson    schedule 18.09.2012
comment
Хорошо, начало объекта хранится в esi, но почему vptr читается дважды? - person sharptooth; 18.09.2012
comment
Также для сохранения vptr потребуется дополнительный регистр, например edi, который затем нужно будет сохранить и восстановить. Улучшит ли это код? Трудно сказать. - person Bo Persson; 18.09.2012

Чтобы сначала ответить на вопрос из заголовка:

Да, объект производного класса меняет свой тип при построении и уничтожении. Это единственный случай.

Код в теле вопроса другой. Но как правильно замечает Максим, у вас просто указатель. Этот указатель может указывать (в разное время) на два разных объекта, находящихся по одному и тому же адресу.

person MSalters    schedule 18.09.2012