Существуют ли случаи, когда можно определить понижение фактической базы до производной?

В общем случае это (очень заслуженное) Undefined Behavior для преобразования вниз от (динамического) Base к одному из производных классов Derived

Очевидный UB

class Base
{
public:
    virtual void foo()
    { /* does something */ }

    int a;
}

class Derived : public Base
{
public:
    virtual void foo()
    { /* does something different */ }

    double b;
}

Base obj;
Derived derObj = *static_cast<Derived *>(&obj);  // <- here come the demons

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

Не столь очевидный наивный случай

Тем не менее, мне было любопытно узнать, были ли какие-то уступки этому правилу в конкретных случаях? Например:

class Base
{
public:
    void foo()
    { /* does something */ }

    int a = 1;
    double b = 2.;
}

class DerivedForInt : public Base
{
    int getVal()
    { return a }
}

Base obj;
DerivedForInt derObj = *static_cast<DerivedForInt *>(&obj);  // <- still an UB ?

Здесь мы легко можем представить, что компилятор делает правильные вещи. Но со стандартной точки зрения он все еще не определен?

Изменить: static_cast - это случайный выбор для иллюстрации, он также интересен при работе с другими приведениями!


person Ad N    schedule 28.11.2013    source источник
comment
Это все еще неопределенное поведение. obj не DerivedForInt.   -  person Simple    schedule 28.11.2013
comment
@Simple Было бы логично, если бы в стандарте не было никаких исключений. Если вы опубликуете его как ответ (возможно, с некоторыми подтверждающими ссылками / стандартным отрывком), это будет очень хорошим принятым ответом; )   -  person Ad N    schedule 28.11.2013
comment
Хорошо, несмотря на то, что у меня есть 2 ответа, в которых говорится, что это должно быть неопределенное поведение, у меня немного другое мнение. Пока оба ваших класса имеют стандартный макет, а производный класс не добавляет новые поля, это должно действительно работать. По крайней мере, вы можете переинтерпретировать_каст согласно этим источникам: stackoverflow.com/questions/4178175/ stackoverflow.com/questions / 8864311 / или я упустил момент?   -  person user1781290    schedule 28.11.2013
comment
@ user1781290 Это очень интересно! Если вы спросите меня, он заслуживает развернутого ответа; )   -  person Ad N    schedule 28.11.2013


Ответы (3)


Хорошо, я, наверное, разорвусь на части за этот ответ ...

Очевидно, что, как указано в других ответах, это поведение undefined, как указано в стандарте. Но если ваш Base класс имеет стандартный макет, а ваш DerivedForInt класс не добавляет новые элементы данных, он будет иметь такой же (стандартный) макет.

В этих условиях ваша гипсовая повязка не должна вызывать проблем, даже будучи UB. Согласно одному из источников, как минимум безопасно делать

DerivedForInt *derived = reinterpret_cast<DerivedForInt*>(&base.a);

Источники:

Что такое агрегаты и POD и как / почему они особенные?

POD и наследование в С ++ 11. Имеет ли адрес struct == адрес первого члена?

По второй ссылке:

Вот определение из стандартного раздела 9 [класс]:

Класс стандартного макета - это класс, который:

  • не имеет нестатических элементов данных типа нестандартного класса макета (или массива таких типов) или ссылки,
  • не имеет виртуальных функций (10.3) и виртуальных базовых классов (10.1),
  • имеет одинаковый контроль доступа (раздел 11) для всех нестатических элементов данных,
  • не имеет базовых классов нестандартной компоновки,
  • либо не имеет нестатических членов данных в наиболее производном классе и не более одного базового класса с нестатическими членами данных, либо не имеет базовых классов с нестатическими членами данных, и
  • не имеет базовых классов того же типа, что и первый нестатический член данных.

И тогда гарантируется желаемое свойство (раздел 9.2 [class.mem]):

Указатель на объект структуры стандартной компоновки, соответствующим образом преобразованный с использованием reinterpret_cast, указывает на его начальный член (или, если этот член является битовым полем, то на единицу, в которой он находится) и наоборот.

Это на самом деле лучше, чем старое требование, потому что возможность reinterpret_cast не теряется при добавлении нетривиальных конструкторов и / или деструктора.

person user1781290    schedule 28.11.2013
comment
Спасибо, что рискнули; ) Я действительно считаю, что ваш ответ может привести к конструктивному обсуждению! Чтобы помочь нам, не могли бы вы извлечь цитату из ваших источников, которая позволяет вам сделать заявление о безопасности reinterpret_cast? - person Ad N; 28.11.2013
comment
Смелый ответ, и определенно тот, который способствует обсуждению. Теперь, когда я понимаю, что вы имеете в виду, моя проблема с этим составом состоит в том, что я не вижу ситуации, в которой это актуально. Единственная причина предпочесть этот производный класс базовому - это использование функций-членов, определенных в производном классе. Если это то, что вы хотите, использование базового класса и вид, что вы этого не делаете, намекают на недостаток дизайна, который, скорее всего, можно исправить. - person Agentlien; 28.11.2013
comment
@Agentlien Это определенно не обычный случай. Было бы интересно унаследовать от класса библиотеки и добавить некоторые аксессоры, я полагаю - person user1781290; 28.11.2013
comment
@ user1781290 Да, я так считал. Однако в этом случае я бы предпочел либо использовать композицию, либо определять функции как бесплатные функции, а не функции-члены. - person Agentlien; 28.11.2013
comment
Цитата редактируется сейчас. @Agentlien В большинстве случаев это, наверное, вопрос вкуса? - person user1781290; 28.11.2013
comment
Из предоставленной вами цитаты единственное, что я могу определить, это: Base base; int *a = reinterpret_cast<int *>(base), а затем (наоборот) Base *b = reinterpret_cast<Base *>(a). Я не вижу ничего по поводу наследования классов. - person Ad N; 28.11.2013
comment
Это правильно. Но если ваш Base соответствует стандартному макету, ваш Derived также будет таким, если вы не добавите новые элементы данных (поскольку это не нарушает ни одного из пунктов для стандартного макета). Следовательно, у обоих классов должна быть одинаковая структура памяти, чтобы сделать доступ возможным. - person user1781290; 28.11.2013
comment
@Agentlien Composing или бесплатные функции не дают вам доступа к защищенным членам, в отличие от наследования (я думаю, это то, что подразумевается под «добавлением средств доступа»). Но это определенно не наш вариант использования: мы пытаемся делать что-то в этом направлении, чтобы попытаться имитировать полиморфизм, но с семантикой значений. Ваше предложение по конверсии ctor в Derived взяв Base может быть тем, что нам действительно нужно. - person Ad N; 28.11.2013
comment
@ user1781290 Действительно, я упустил главную часть рассуждений, касающуюся последствий стандартного макета! (небольшое примечание: я думаю, вы забыли взять адрес члена a в вашем примере кода, и SO не позволил бы мне внести изменения с одной разницей в символах). - person Ad N; 28.11.2013
comment
Обратите внимание, что здесь используется reinterpret_cast вместо static_cast, как предполагалось в вопросе. - person MSalters; 28.11.2013
comment
@MSalters Из-за стандартной компоновки обоих классов static_cast также должен работать - person user1781290; 28.11.2013
comment
@ user1781290, почему должен быть UB, если и Base, и DerivedForInt являются стандартными? - person Konstantin Oznobihin; 11.12.2013
comment
@KonstantinOznobihin Согласно другим ответам, это UB по стандарту С ++. Но я не вижу случая, чтобы это не сработало - person user1781290; 11.12.2013
comment
@ user1781290 о, я понимаю, это цитаты из стандарта C ++ 2003 (в котором нет понятия стандартного типа макета, кстати), но для C ++ 11 не должно быть UB. - person Konstantin Oznobihin; 11.12.2013

n3376 5.2.9/11

A prvalue of type “pointer to cv1 B,” where B is a class type, can be converted to a prvalue of type “pointer to cv2 D,” where D is a class derived (Clause 10) from B if a valid standard conversion from “pointer to D” to “pointer to B” exists (4.10), cv2 is the same cv-qualification as, or greater cv-qualification than, cv1, and B is neither a virtual base class of D nor a base class of a virtual base class of D. The null pointer value (4.10) is converted to the null pointer value of the destination type.

Если prvalue типа «указатель на cv1 B» указывает на B, который на самом деле является подобъектом объекта типа D, результирующий указатель указывает на включающий объект типа D. В противном случае результатом преобразования будет не определено.

Since &obj is not points to DerivedForInt it's UB.

person ForEveR    schedule 28.11.2013

Это все еще неопределенное поведение, и я считаю, что так и должно быть.

Почему не определено

Как сообщил @ForEveR в своем ответе:

n3376 5.2.9/11

Prvalue типа «указатель на cv1 B», где B - тип класса, может быть преобразовано в prvalue типа «указатель на cv2 D», где D - класс, производный (раздел 10) от B.

...

Если prvalue типа «указатель на cv1 B» указывает на B, который на самом деле является подобъектом объекта типа D, результирующий указатель указывает на включающий объект типа D. В противном случае результатом преобразования будет не определено.

Почему это не должно быть определено

Это будет работать только для типов POD, так как добавления виртуальной функции к вашей базе достаточно, чтобы это навредило вам во всех компиляторах, о которых я знаю. Кроме того, разница между типами может быть концептуальной, а не только в расположении данных. Безопасность типов - это не только обеспечение сильной абстракции, но и предотвращение проблем с представлением данных.

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

person Agentlien    schedule 28.11.2013