Несоответствие адреса 'this', когда базовый класс не полиморфен, но является производным

Вот такой код:

#include <iostream>

class Base
{
public:
    Base() {
        std::cout << "Base: " << this << std::endl;
    }
    int x;
    int y;
    int z;
};

class Derived : Base
{
public:
    Derived() {
        std::cout << "Derived: " << this << std::endl;
    }

    void fun(){}
};

int main() {
   Derived d;
   return 0;
}

Выход:

Base: 0xbfdb81d4
Derived: 0xbfdb81d4

Однако, когда функция fun изменена на виртуальную в классе Derived:

virtual void fun(){} // changed in Derived

Тогда адрес this в обоих конструкторах не совпадает:

Base: 0xbf93d6a4
Derived: 0xbf93d6a0

Другое дело, если класс Base полиморфен, например, я добавил туда другую виртуальную функцию:

virtual void funOther(){} // added to Base

то адреса обоих 'this' снова совпадают:

Base: 0xbfcceda0
Derived: 0xbfcceda0

Возникает вопрос: почему этот адрес отличается в базовом и производном классе, если базовый класс не полиморфен, а производный класс - нет?


person scdmb    schedule 21.07.2012    source источник
comment
Я предполагаю, что первый - это случай оптимизации пустого базового класса, а второй - из-за наличия vptr в производном классе   -  person Mr.Anubis    schedule 21.07.2012
comment
Разница всего в 4 байта, возможно, размер указателя. Добавление функции fun заставляет иметь указатель на нее (ну, не совсем на нее, но не имеет значения) в Derived, который не отображается в Base, отсюда и разница. Так что это может быть связано с наличием vtable в Derived (en.wikipedia.org/wiki/Virtual_method_table < / а>)   -  person BlackBear    schedule 21.07.2012
comment
@ Мистер Анубис: Но где же в этом случае пустой базовый класс? Единственный базовый класс - Base, и он никогда не бывает пустым.   -  person AnT    schedule 21.07.2012
comment
@AndreyT Я подумал то же самое через некоторое время, разместив комментарий (наверное, это нужно было назвать оптимизацией). Так идиот с моей стороны: D   -  person Mr.Anubis    schedule 21.07.2012


Ответы (3)


Когда у вас есть полиморфная иерархия классов с одинарным наследованием, типичное соглашение, которому следуют большинство (если не все) компиляторы, заключается в том, что каждый объект в этой иерархии должен начинаться с указателя VMT (указателя на таблицу виртуальных методов). В таком случае указатель VMT вводится в структуру объектной памяти раньше: корневым классом полиморфной иерархии, в то время как все низшие классы просто наследуют его и устанавливают так, чтобы он указывал на свой собственный VMT. В таком случае все вложенные подобъекты в любом производном объекте имеют одинаковое значение this. Таким образом, считывая ячейку памяти в *this, компилятор получает немедленный доступ к указателю VMT независимо от фактического типа подобъекта. Именно это и произошло в вашем последнем эксперименте. Когда вы делаете корневой класс полиморфным, все значения this совпадают.

Однако, когда базовый класс в иерархии не полиморфен, он не вводит указатель VMT. Указатель VMT будет представлен самым первым полиморфным классом где-то ниже в иерархии. В таком случае популярным подходом реализации является вставка указателя VMT перед данными, введенными неполиморфной (верхней) частью иерархии. Это то, что вы видите во втором эксперименте. Схема памяти для Derived выглядит следующим образом

+------------------------------------+ <---- `this` value for `Derived` and below
| VMT pointer introduced by Derived  |
+------------------------------------+ <---- `this` value for `Base` and above
| Base data                          |
+------------------------------------+
| Derived data                       |
+------------------------------------+

Между тем, все классы в неполиморфной (верхней) части иерархии не должны ничего знать о каких-либо указателях VMT. Объекты типа Base должны начинаться с поля данных Base::x. При этом все классы в полиморфной (нижней) части иерархии должны начинаться с указателя VMT. Чтобы удовлетворить оба этих требования, компилятор вынужден корректировать значение указателя объекта по мере его преобразования вверх и вниз по иерархии из одного вложенного базового подобъекта в другой. Это сразу означает, что преобразование указателя через полиморфную / неполиморфную границу больше не является концептуальным: компилятор должен добавить или вычесть некоторое смещение.

Подобъекты из неполиморфной части иерархии будут иметь общее значение this, в то время как подобъекты из полиморфной части иерархии будут иметь собственное, другое значение this.

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

Эффект сложения / вычитания также будет обнаружен при преобразовании указателя.

Derived *pd = new Derived;
Base *pb = pd; 
// Numerical values of `pb` and `pd` are different if `Base` is non-polymorphic
// and `Derived` is polymorphic

Derived *pd2 = static_cast<Derived *>(pb);
// Numerical values of `pd` and `pd2` are the same
person AnT    schedule 21.07.2012
comment
В случае сравнения указателей (на один и тот же объект) неполиморфного базового и полиморфного объекта производного класса указатели сравниваются как и ожидалось. Как это работает? Пример: во втором случае, описанном выше, я сравниваю Base * с Derived *. - person Agnel Kurian; 21.07.2012
comment
@Agnel Kurian: Я не уверена, что понимаю, о чем вы говорите. В реализации OP указатели в регистре Base *pb = pd; должны численно отличаться, если Base не полиморфен, а Derived полиморфен. (Необходимо убедиться, что наследование является общедоступным. В противном случае оно просто не будет компилироваться.) - person AnT; 21.07.2012
comment
Во втором случае (невиртуальные Base и виртуальные Derived классы), если я создаю объект d из Derived и сохраняю его адрес как Base *pb = &d; и Derived *pd = &d, а затем сравниваю указатели как (pb == pd), сравнение возвращает true. Как это работает, если указатели this разные? - person Agnel Kurian; 21.07.2012
comment
@Agnel Kurian: Когда вы делаете pb == pd сравнение, компилятор видит, что типы указателей разные. В языке говорится, что Base * должен использоваться как общий тип для сравнения, т.е. что pd в этом случае необходимо преобразовать в тип Base *. Другими словами, ваш pb == pd интерпретируется как pb == (Base *) pd. Преобразование - это то, что регулирует правый указатель перед сравнением. Вы действительно не сравниваете числовые значения указателей на pb == pd. - person AnT; 22.07.2012
comment
@Agnel Kurian: Попробуйте это в своем примере: uintptr_t nb = (uintptr_t) pb, nd = (uintptr_t) pd;, а затем распечатайте значения nb и nd. Вы увидите, что они разные. Однако pb == pd все равно вернет true. - person AnT; 22.07.2012

Это похоже на поведение типичной реализации полиморфизма с указателем v-таблицы в объекте. Базовый класс не требует такого указателя, поскольку у него нет виртуальных методов. Это экономит 4 байта в размере объекта на 32-битной машине. Типичный макет:

+------+------+------+
|   x  |   y  |   z  |
+------+------+------+

    ^
    | this

Однако класс Derived действительно требует указателя v-таблицы. Обычно сохраняется со смещением 0 в компоновке объекта.

+------+------+------+------+
| vptr |   x  |   y  |   z  |
+------+------+------+------+

    ^
    | this

Таким образом, чтобы заставить методы базового класса видеть тот же макет объекта, генератор кода добавляет 4 к указателю this перед вызовом метода базового класса. Конструктор видит:

+------+------+------+------+
| vptr |   x  |   y  |   z  |
+------+------+------+------+
           ^
           | this

Это объясняет, почему вы видите, что к значению указателя this в конструкторе Base добавлено 4.

person Hans Passant    schedule 21.07.2012
comment
это очень интересно. Итак, допустим, мы используем размещение new в полиморфной иерархии с некоторыми неполиморфными базами (также включенными). Наш расчет для данного адреса - это просто максимальное требование выравнивания, но мы не беспокоимся о сохранении значения, возвращаемого новым размещением. Можем ли мы безопасно повторно интерпретировать_приведение нашего адреса памяти к любому T * родительской иерархии? - person v.oddou; 10.04.2015

С технической точки зрения, как раз и происходит.

Однако следует отметить, что согласно спецификации языка, реализация полиморфизма не обязательно связана с vtables: это то, что указано в spec. определяется как «деталь реализации», которая выходит за рамки спецификации.

Все, что мы можем сказать, это то, что this имеет тип и указывает на то, что доступно через его тип. То, как происходит разыменование на члены, опять же, является деталью реализации.

Тот факт, что pointer to something при преобразовании в pointer to something else, будь то неявное, статическое или динамическое преобразование, должен быть изменен с учетом того, что происходит вокруг, должен рассматриваться как правило, а не исключение .

Между тем, как определяется C ++, вопрос бессмысленный, как и ответы, поскольку они неявно предполагают, что реализация основана на предполагаемых макетах.

Тот факт, что в данных обстоятельствах два субкомпонента объекта имеют одно и то же происхождение, является лишь (очень распространенным) частным случаем.

Исключением является «переинтерпретация»: когда вы «ослепляете» систему типов и просто говорите «посмотрите на эту кучу байтов, поскольку они являются экземпляром этого типа»: это единственный случай, когда вы не должны ожидать изменения адреса (и никакой ответственности от компилятора о значимости такого преобразования).

person Emilio Garavaglia    schedule 21.07.2012