Memcpy производный класс в базовый класс, почему до сих пор вызывается функция базового класса

я читаю Внутри объектной модели C ++. В разделе 1.3

Итак, почему же это, учитывая

Bear b;
ZooAnimal za = b;

// ZooAnimal::rotate() invoked
za.rotate();

Вызванный экземпляр rotate () — это экземпляр ZooAnimal, а не экземпляр Bear? Более того, если инициализация по элементам копирует значения одного объекта в другой, почему vptr za не обращается к виртуальной таблице Bear?

Ответ на второй вопрос заключается в том, что компилятор ходатайствует об инициализации и присваивании одного объекта класса другому. Компилятор должен убедиться, что если объект содержит один или несколько vptrs, эти значения vptr не инициализируются и не изменяются исходным объектом .

Итак, я написал тестовый код ниже:

#include <stdio.h>
class Base{
public:
virtual void vfunc() { puts("Base::vfunc()"); }
};
class Derived: public Base
{
public:
virtual void vfunc() { puts("Derived::vfunc()"); }
};
#include <string.h>

int main()
{
Derived d;
Base b_assign = d;
Base b_memcpy;
memcpy(&b_memcpy, &d, sizeof(Base));

b_assign.vfunc();
b_memcpy.vfunc();

printf("sizeof Base : %d\n", sizeof(Base));

Base &b_ref = d;
b_ref.vfunc();

printf("b_assign: %x; b_memcpy: %x; b_ref: %x\n",
*(int *)&b_assign,
*(int *)&b_memcpy,
*(int *)&b_ref);
return 0;
}

результат

Base::vfunc()
Base::vfunc()
sizeof Base : 4
Derived::vfunc()
b_assign: 80487b4; b_memcpy: 8048780; b_ref: 8048780

У меня вопрос, почему b_memcpy по-прежнему называется Base :: vfunc ()

1

Решение

То, что вы делаете, является недопустимым на языке C ++, что означает, что поведение вашего b_memcpy объект не определен Последнее означает, что любое поведение является «правильным», а ваши ожидания полностью необоснованны. Нет смысла пытаться анализировать неопределенное поведение — оно не должно следовать какой-либо логике.

На практике вполне возможно, что ваши манипуляции с memcpy действительно скопировал Derivedуказатель на виртуальную таблицу b_memcpy объект. И ваши эксперименты с b_ref подтвердите это. Однако, когда виртуальный метод вызывается через непосредственный объект (как в случае с b_memcpy.vfunc() вызов) большинство реализаций оптимизируют доступ к виртуальной таблице и выполняют непосредственный (невиртуальном) вызов целевой функции. Формальные правила языка гласят, что никакие юридические действия не могут быть совершены b_memcpy.vfunc() позвонить, чтобы отправить к чему-либо, кроме Base::vfunc()Именно поэтому компилятор может смело заменить этот вызов прямым вызовом Base::vfunc(), Вот почему любые манипуляции с виртуальными таблицами обычно не влияют на b_memcpy.vfunc() вызов.

2

Другие решения

Вызванное вами поведение не определено, потому что стандарт говорит, что оно не определено, и ваш компилятор использует этот факт. Давайте посмотрим на g ++ для конкретного примера. Сборка, которую он генерирует для линии b_memcpy.vfunc(); с отключенной оптимизацией выглядит так:

lea     rax, [rbp-48]
mov     rdi, rax
call    Base::vfunc()

Как видите, на vtable даже не ссылались. Поскольку компилятор знает статический тип b_memcpy у него нет причин отправлять вызов этого метода полиморфно. b_memcpy не может быть ничего, кроме Base объект, поэтому он просто генерирует вызов Base::vfunc() как это было бы с любым другим вызовом метода.

Пройдя немного дальше, давайте добавим такую ​​функцию:

void callVfunc(Base& b)
{
b.vfunc();
}

Теперь, если мы позвоним callVfunc(b_memcpy); мы можем увидеть разные результаты. Здесь мы получаем другой результат в зависимости от уровня оптимизации, на котором я компилирую код. На -O0 и -O1 Derived::vfunc() называется и на -O2 и -O3 Base::vfunc() печатается. Опять же, поскольку стандарт гласит, что поведение вашей программы не определено, компилятор не прилагает усилий для получения предсказуемого результата и просто полагается на предположения, сделанные языком. Поскольку компилятор знает b_memcpy это Base объект, он может просто встроить вызов puts("Base::vfunc()"); когда уровень оптимизации позволяет это сделать.

1

Вам не разрешено делать

memcpy(&b_memcpy, &d, sizeof(Base));

— это неопределенное поведение, потому что b_memcpy а также d не являются объектами «простых старых данных» (так как у них есть виртуальные функции-члены).

Если вы написали:

b_memcpy = d;

тогда это напечатало бы Base::vfunc() как и ожидалось.

0

Конечно, использование memcpy здесь есть UB

Ответы, указывающие на то, что любое использование memcpyили другое манипулирование байтами не-POD, то есть любого объекта с vptr, имеет неопределенное поведение, строго технически правильно, но не отвечает на вопрос. Вопрос основан на существовании vptr (указатель vtable), который даже не предписан стандартом: конечно, ответ будет включать факты вне стандарта, и результат счета не будет гарантирован стандартом!

Стандартный текст не имеет отношения к vptr

Проблема не в том вы не разрешено манипулировать vptr; идея о том, что стандарт допускает манипулирование чем-либо, что даже не описано в стандартном тексте, абсурдно. Конечно, не стандартный способ изменить vptr будет существовать, и это не относится к делу.

Vptr кодирует тип полиморфного объекта

Проблема здесь не в том, что стандарт говорит о vptr, вопрос в том, что представляет vptr, и что стандарт говорит об этом: vptr представляет динамический тип объекта. Всякий раз, когда результат операции зависит от динамического типа, компилятор сгенерирует код для использования vptr.

[Примечание относительно MI: я говорю «the» vptr (как будто только один vptr), но когда задействован MI (множественное наследование), объекты могут иметь более одного vptr, каждый из которых представляет полный объект, рассматриваемый как определенный полиморфный базовый класс тип. (Полиморфный класс — это класс с хотя бы одной виртуальной функцией.)] [Примечание относительно виртуальных баз: я упоминаю только vptr, но некоторые компиляторы вставляют другие указатели для представления аспектов динамического типа, таких как расположение субобъектов виртуальной базы, а некоторые другие компиляторы используют vptr для этой цели. То, что верно в отношении vptr, верно и в отношении этих других внутренних указателей.]

Так конкретное значение vptr соответствует динамическому типу: это тип самого производного объекта.

Во время построения динамический тип меняется, и поэтому вызовы виртуальных функций из конструктора могут быть «удивительными». Некоторые люди говорят, что правила вызова виртуальных функций во время конструирования являются особыми, но это абсолютно не так: вызывается окончательный переопределитель; это переопределение — это класс, соответствующий самому производному объекту, который был построен, и в конструкторе C::C(arg-list), это всегда тип класса C,

Во время уничтожения динамический тип изменяется в обратном порядке. Призывы к виртуальной функции изнутри деструкторов следуют тем же правилам.

Вы можете делать низкоуровневые манипуляции, которые не разрешены стандартом. То, что поведение не определено явно в стандарте C ++, не означает, что оно не описано в другом месте.. То, что результат манипуляции явно описан с наличием UB (неопределенное поведение) в стандарте C ++, не означает, что ваша реализация не может его определить.

Вы также можете использовать свои знания о том, как работают компиляторы: если используется строгая раздельная компиляция, то есть когда компилятор не может получить информацию из отдельно скомпилированного кода, каждая отдельно скомпилированная функция является «черным ящиком». Вы можете использовать этот факт: компилятор должен будет предположить, что все, что может сделать отдельно скомпилированная функция, будет выполнено. Даже внутри данной функции, вы можете использовать asm директива, чтобы получить те же эффекты: asm Директива без ограничений может делать все, что допустимо в C ++. Результатом является директива «забудьте то, что вы знаете из анализа кода в этот момент».

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

Вызов конструкторов для существующего объекта не допускается, кроме как для восстановления его с точно таким же типом (и с ограничениями), см. [Basic.life] / 8 :

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

(8.1) хранилище для нового объекта точно перекрывает хранилище
место, которое занимал исходный объект, и

(8.2) новый объект того же типа, что и исходный объект
(игнорируя cv-квалификаторы верхнего уровня), и

(8.3) тип исходного объекта не является константным, и, если
тип класса, не содержит ни одного нестатического члена данных, тип которого
является константным или ссылочным типом, и

(8.4) исходный объект был наиболее производным ([intro.object])
типа T и новый объект является наиболее производным объектом типа T (что
то есть они не являются подобъектами базового класса).

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

Другими словами, если вы хотите перезаписать vptr, используя низкоуровневые трюки, вы можете; но только если вы напишите то же значение.

Другими словами, не пытайтесь взломать vptr.

0
По вопросам рекламы [email protected]