ковариантные возвращаемые типы с множественным наследованием. как работает этот код? - PullRequest
10 голосов
/ 28 июля 2011

Может кто-нибудь сказать мне, как ковариация типа возврата работает в следующем коде?

class X
{
public:
    int x;
};

class Y: public OtherClass, public X
{
};


static Y inst;

class A {
public:
    virtual X* out() = 0;
};

class B : public A
{
    public:
    virtual Y* out() 
    {
         return &inst;
    }
};



void main()
{
    B b; 
    A* a = &b;
    //x and y have different addresses. how and when is this conversion done??
    Y* y = b.out();
    X* x = a->out();
}

РЕДАКТИРОВАТЬ: я извиняюсь, я не был достаточно ясным.x и y указывают на разные адреса, как я и ожидал, так как здесь задействовано множественное наследование, поэтому объекты X и Y не находятся на одном и том же адресе.Мой вопрос, когда этот актерский состав делается?Функция out () не смогла бы этого сделать, потому что она всегда возвращает указатель на Y с ее точки зрения.Вызывающая функция out () не могла этого сделать, потому что видит X *, конкретным типом которого может быть X или Y. Когда тогда выполняется приведение?

Ответы [ 5 ]

3 голосов
/ 28 июля 2011

Под словом "как это работает" я полагаю, вы спрашиваете, как выглядит сгенерированный код.(Конечно, в типичной реализации. Мы все знаем, что сгенерированный код может несколько отличаться в разных реализациях.) Мне известны две возможные реализации:

Компилятор всегда генерирует код для возврата указателя на базуучебный класс;то есть в B::out компилятор преобразует Y* в X* перед его возвратом.На месте вызова, если вызов был через lvalue со статическим типом B, компилятор сгенерирует код для преобразования возвращенного значения в Y*.

В качестве альтернативы (и я думаю, что это чаще,но я не совсем уверен), компилятор генерирует thunks, поэтому, когда вы вызываете a->out, виртуальная функция, которая вызывается не напрямую B::out, а маленькая оболочка, которая преобразует Y*, возвращаемый из B::outX*.

Кажется, что и g ++, и VC ++ используют thunks (с очень быстрого взгляда).

3 голосов
/ 28 июля 2011

Когда произойдет приведение?

Что ж, это делается после возврата B::out и до завершения вызова A::out.Там на самом деле не так много.

Указатель, возвращаемый полиморфным вызовом, должен иметь тип X*.Это означает, что он должен указывать на объект типа X.Поскольку вы используете множественное наследование, ваши Y объекты могут содержать несколько подобъектов.В вашем случае они могут иметь подобъект OtherClass и подобъект X.Очевидно, что эти подобъекты не хранятся в одном и том же адресе.Когда вы запрашиваете указатель на X*, вы получаете указатель на подобъект X, а когда вы запрашиваете указатель на Y*, вы получаете указатель на весь объект Y.

class OtherClass { int a; };

// the other classes

int main()
{
    B b; 
    A* a = &b;
    //x and y have different addresses. how and when is this conversion done??
    Y* y = b.out();
    X* x = a->out();
    OtherClass* o = y;
    std::cout << "x: " << x << std::endl;
    std::cout << "y: " << y << std::endl;
    std::cout << "o: " << o << std::endl;
}

Вы можете видеть, что при выполнении этого кода дает разные адреса для указателей X* и Y* и один и тот же адрес для указателей Y* и OtherClass*, поскольку объект был размечен с помощьюподобъект OtherClass перед субобъектом X.

x: 0x804a15c
y: 0x804a158
o: 0x804a158

1 голос
/ 28 июля 2011

ПРИМЕЧАНИЕ: первый ответ верен только для одиночного наследования, множественное наследование ниже (edit2).

для x и y нормально иметь разные адреса, потому что это два разных указателя.Они имеют одно и то же значение, которое является адресом переменной, на которую они указывают.

edit: вы можете использовать это main, чтобы проверить, что я имею в виду, в первой строке будет напечатано значение x и y (то есть адрес, на который они указывают), который всегда должен быть одинаковым, потому что на самом деле один и тот же метод будет вызываться из-за виртуальности out.Второй напечатает свой собственный адрес, который, конечно, отличается, потому что это разные (указатели) переменные.

#include <iostream>
int main()
{
    B b;
    A* a = &b;
    //x and y have different addresses. how and when is this conversion done??
    Y* y = b.out();
    X* x = a->out();
    std::cout << y << std::endl << x << std::endl;
    std::cout << &y << std::endl << &x << std::endl;
    return 0;
}

edit2: хорошо, это было неправильно, мои извинения.Когда в игру вступает множественное наследование, неявное приведение от Y* до X* (то есть при присваивании x, а не при возврате out) изменит адрес указателя.

Это происходит потому, что при реализации макет Y содержит 2 дополнительные реализации класса, являющиеся частью, унаследованной от OtherClass, и частью, унаследованной от X.При неявном приведении к X* (что разрешено) адрес должен измениться, чтобы указать на X -часть Y, поскольку X не знает OtherClass.

0 голосов
/ 28 июля 2011

В зависимости от того, как выглядит OtherClass, значения, содержащиеся в указателях x и y, действительно могут численно отличаться, хотя они указывают на один и тот же объект - причиной этого (для многих удивительного) факта является множественное наследование. Чтобы понять причину, по которой это может быть, вы должны рассмотреть структуру памяти объектов класса Y (см. Также Запись в Википедии по VTable ). Предположим, что класс OtherClass выглядит следующим образом:

class OtherClass
{
    public:
    virtual ~OtherClass() {}
};

Тогда экземпляр класса Y может, в зависимости от вашей системы и компилятора, выглядеть следующим образом:
0x0000 ... 4 байта - int x (унаследовано от класса X)
0x0004 ... 4 байта - указатель на vtable класса Y (для базы OtherClass)

В следующей строке вашего кода происходит преобразование указателя на Y в указатель на X:

X* x = a->out();

Результирующий адрес, хранящийся в x, теперь указывает на 4 байта перед адресом, сохраненным в y. Поскольку компилятор знает структуру памяти, он также знает смещения, используемые при преобразовании ковариантных типов, и помещает соответствующие дополнения / вычитания в скомпилированный файл. Однако обратите внимание, что даже если числовые указатели отличаются, они будут преобразованы в общий тип при сравнении, и такое сравнение вернет 1.

0 голосов
/ 28 июля 2011

Поскольку Y* можно преобразовать в X*, код работает нормально.Когда вы вызываете out() с A*, тогда возвращаемое значение B::out() разрешается только в X*.

Из-за неявного преобразования из Y* в X* вы не замечаете измененияили проблема типа return.Просто попробуйте выполнить следующие 2 сценария, и ваш код перестанет работать:

  1. изменив X* на int*
  2. сделайте наследование с X до Y как private/ protected
...