Базовый указатель на массив производных объектов

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

struct Base
{
  int member;
};

struct Derived : Base
{
  int another_member;
};

int main()
{
  Base* p = new Derived[10]; // (1)
  p[1].member = 42; // (2)
  delete[] p; // (3)
}

В соответствии со стандартом (1) имеет правильный формат, потому что Dervied* (которое является результатом new-expression) может быть неявно преобразовано в Base* (черновик C++11, §4.10/3):

Значение prvalue типа «указатель на cv D», где D — тип класса, может быть преобразовано в значение prvalue типа «указатель на cv B», где B — это базовый класс (пункт 10) D. Если B является недоступным (пункт 11) или неоднозначным (10.2) базовым классом D, программа, которая требует этого преобразования, является неправильно сформированной. Результатом преобразования является указатель на подобъект базового класса объекта производного класса. Значение нулевого указателя преобразуется в значение нулевого указателя целевого типа.

(3) приводит к неопределенному поведению из-за §5.3.5/3:

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

Является ли (2) допустимым в соответствии со стандартом или это приводит к неправильной структуре программы или неопределенному поведению?

изменить: улучшить формулировку


person Vitus    schedule 25.08.2011    source источник
comment
Почему мы предполагаем, что (2) неправильно сформировано?   -  person Kerrek SB    schedule 26.08.2011
comment
(2) имеет очень неправильный формат, поскольку использует sizeof(Base) для вычисления расстояния между p[0] и p[1].   -  person Bo Persson    schedule 26.08.2011
comment
Это не неправильно, это просто UB, потому что p не указывает на элемент объекта массива (условие для работы арифметики указателя), он указывает на подобъект базового класса элемента массива, поэтому доступ к массиву недействителен.   -  person CB Bailey    schedule 26.08.2011
comment
@Kerrek SB: Возможно, последний вопрос нужно было сформулировать немного по-другому, но, поскольку основные реализации (протестированные с помощью gcc, clang и MSVC) не понимают его правильно, я предполагаю (2) неправильно сформирован . Я провел последние два часа в поисках чего-то вроде того, что сказал Бо Перссон, то есть (p + n) использует статический тип p для вычисления смещения, но у меня сложилось впечатление, что абзац, касающийся operator+, не подразумевает этого.   -  person Vitus    schedule 26.08.2011
comment
@Charles Bailey: О, это действительно имеет смысл. Пожалуйста, опубликуйте это как ответ.   -  person Vitus    schedule 26.08.2011
comment
Извините, у меня нет доступа к стандарту прямо сейчас, но в противном случае поведение не определено в разделе, посвященном оператору сложения при применении к указателю и целому числу, описывающему требование. (IIRC)   -  person CB Bailey    schedule 26.08.2011
comment
@Charles Bailey: я думаю, что это часть стандарта, о котором вы говорите (§5.7/5): (...) Если и операнд указателя, и результат указывают на элементы одного и того же объекта массива или один после последнего элемента объекта массива оценка не должна приводить к переполнению; в противном случае поведение не определено.   -  person Vitus    schedule 26.08.2011
comment
Строго говоря, если это так, то поведение не определено даже при sizeof(Base) == sizeof(Derived), хотя большинство реализаций будут правильными.   -  person Vitus    schedule 26.08.2011


Ответы (4)


Если вы посмотрите на выражение p[1], p является Base* (Base является полностью определенным типом), а 1 является int, поэтому согласно ISO/IEC 14882:2003 5.2.1 [expr.sub] это выражение допустимо и идентично до *((p)+(1)).

Начиная с версии 5.7 [expr.add]/5, когда к указателю добавляется целое число, результат корректно определяется только тогда, когда указатель указывает на элемент объекта массива, а результат арифметики указателя также указывает на элемент этого объекта. объект массива или один после конца массива. p, однако, не указывает на элемент объекта массива, он указывает на подобъект базового класса объекта Derived. Элементом массива является объект Derived, а не подобъект Base.

Обратите внимание, что в 5.7/4 для целей оператора сложения подобъект Base можно рассматривать как массив единичного размера, поэтому технически вы можете сформировать адрес p + 1, но как указатель «один после последнего элемента» , он не указывает на объект Base, и попытка чтения или записи в него вызовет неопределенное поведение.

person CB Bailey    schedule 26.08.2011
comment
Интересно, не могли бы вы взвесить аналогичный вопрос, который я задавал, в котором Derived не добавляет элементы данных поверх того, что Base уже есть. Я получил некоторые ответы, но мне нужна помощь, чтобы решить, что правильно. - person Rob Kennedy; 09.11.2013
comment
@RobKennedy: я перечитал свой ответ и не понимаю, почему вы думаете, что он применим только тогда, когда sizeof(Derived) != sizeof(Base). - person CB Bailey; 09.11.2013

(3) приводит к неопределенному поведению, но, строго говоря, не является некорректным. Неправильный формат означает, что программа на C++ построена не в соответствии с правилами синтаксиса, диагностируемыми семантическими правилами и правилом одного определения.

То же самое для (2), он правильно сформирован, но не делает того, что вы, вероятно, ожидали. Согласно 8.3.4/6:

За исключением случаев, когда он был объявлен для класса (13.5.5), оператор нижнего индекса [] интерпретируется таким образом, что E1[E2] идентичен *((E1)+(E2)). Из-за правил преобразования, которые применяются к +, если E1 — массив, а E2 — целое число, то E1[E2] относится к E2-му элементу E1. Таким образом, несмотря на асимметричный вид, индексирование является коммутативной операцией.

Таким образом, в (2) вы получите адрес, который является результатом p+sizeof(Base)*1, когда вы, вероятно, хотели получить адрес p+sizeof(Derived)*1.

person Kirill V. Lyadvinsky    schedule 25.08.2011
comment
Глупый я, я читаю undefined и пишу неправильно. Исправлено, спасибо. - person Vitus; 26.08.2011
comment
Дело в том, что §5.7/5, похоже, не подразумевает, что (char*)(p + 1) == (char*)p + sizeof(Base) - если я не ошибаюсь. В этом, я думаю, смысл моего вопроса. - person Vitus; 26.08.2011

Стандарт не запрещает (2), но тем не менее это опасно.

Проблема в том, что выполнение p[1] означает добавление sizeof(Base) к базовому адресу p и использование данных в этом месте памяти в качестве экземпляра Base. Но очень высока вероятность того, что sizeof(Base) меньше, чем sizeof(Derived), поэтому вы будете интерпретировать блок памяти, начинающийся в середине объекта Derived, как объект Base.

Дополнительные сведения см. в часто задаваемых вопросах по C++ Lite 21.4.

person Sander De Dycker    schedule 25.08.2011

p[1].member = 42; 

хорошо формируется. Статический тип для pDerived, а динамический тип — Base. p[1] эквивалентен *(p+1), который кажется допустимым и является указателем на первый элемент динамического типа Base в массиве.

Однако на самом деле *(p+1) относится к члену массива типа Derived. Код p[1].member = 42; показывает, что вы думаете, что имеете в виду элемент массива с типом Base.

person kiriloff    schedule 04.05.2013