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

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

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

Bear b; 
ZooAnimal za = b; 

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

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

Ответ на второй вопрос заключается в том, что компилятор вмешивается в инициализацию и присвоение одного объекта класса другому. Компилятор должен гарантировать, что если объект содержит один или несколько vptr, эти значения 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 ()


person Divlaker    schedule 12.12.2016    source источник
comment
Я полагаю, ответ - дизассемблер, а подсказка - &b_memcpy == &b_ref   -  person fghj    schedule 12.12.2016
comment
Поведение не определено. Любой результат, который вы видите, может легко отличаться с другим компилятором, другими параметрами компилятора, оптимизациями и т. Д.   -  person PaulMcKenzie    schedule 12.12.2016
comment
Я изменил свой тестовый код на this, и результат оказался таким, как я ожидал.   -  person Divlaker    schedule 12.12.2016
comment
@PaulMcKenzie vptr даже официально не существует, поэтому любые манипуляции с vptr будут неопределенными или UB. Тем не менее, некоторые манипуляции должны работать, если мы можем правдоподобно утверждать, что правила не были нарушены.   -  person curiousguy    schedule 28.01.2017


Ответы (4)


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

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

person AnT    schedule 12.12.2016
comment
мой последний printf выводит значение vptr для b_assign, b_memcpy, b_ref, вы можете видеть, что vptr b_memcpy и b_ref ​​одинаковы, поэтому они указывают на одну и ту же vtable, но при вызове vfunc () результат не тот. - person Divlaker; 12.12.2016
comment
@Divlaker - GIGO. - person PaulMcKenzie; 12.12.2016
comment
@Divlaker: Ты действительно читал ответ? Как я сказал выше, вызовы, сделанные напрямую через b_memcpy, не используют vtable и вообще не заботятся о vtable. Не имеет значения, что такое vptr b_memcpy. Но звонки через b_ref используют vtable. Вот почему результат другой. - person AnT; 12.12.2016
comment
@AnT Спасибо. Я понял. - person Divlaker; 12.12.2016
comment
немедленный автоматический? - person curiousguy; 14.05.2018

Вызванное вами поведение не определено, потому что в стандарте указано, что оно не определено, и ваш компилятор пользуется этим фактом. Давайте посмотрим на 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()");, если это позволяет уровень оптимизации.

person Miles Budnek    schedule 12.12.2016
comment
Спасибо за ваш ответ. Я понял. Только использование указателя или ссылки может вызвать vptr. - person Divlaker; 12.12.2016

Тебе нельзя делать

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

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

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

b_memcpy = d;

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

person user253751    schedule 12.12.2016
comment
Я просто проверяю свою идею. Вы можете видеть, что sizeof (Base) равно 4, поэтому я просто копирую указатель vptr. - person Divlaker; 12.12.2016
comment
@Divlaker Действительно, но вам не разрешено копировать указатель vptr. - person user253751; 12.12.2016
comment
@immibis Официально вам не разрешено это делать, пользователи могут. У вас должен быть предлог, чтобы запустить ctor. - person curiousguy; 28.01.2017
comment
@immibis Я не совсем понимаю, что вы имеете в виду под копией. Я знаю чтение и запись, а не копии. - person curiousguy; 28.01.2017
comment
@curiousguy Я имею в виду то, что вы имели в виду. Вы тот, кто сказал, что ctors могут [скопировать указатель vptr] - person user253751; 28.01.2017
comment
@immibis Нет. Я ничего не сказал о копировании, так как не знаю, что считается одним. x=y=0; копия? Насчет y=0; x=0; Я сказал, что только ctors могут изменять vptr. Это изменение может считаться копией, если вы (и ваш бог) захотите. - person curiousguy; 28.01.2017
comment
@curiousguy Копирование чего-либо означает чтение значения из одного объекта и запись его в другой объект. В этом случае он будет читать vptr d и записывать его в b_memcpy. Я также нахожу довольно смешным, что вы не знаете здесь значения слова «копия», тем более что вопрос иллюстрирует это с помощью кода. - person user253751; 28.01.2017
comment
Позвольте нам продолжить это обсуждение в чате. - person curiousguy; 28.01.2017

Любое использование vptr выходит за рамки стандарта.

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

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

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

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

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

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

[Примечание относительно MI: я говорю «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 (то есть они не являются подобъектами базового класса).

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

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

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

person curiousguy    schedule 28.01.2017