12

Есть класс A, от которого наследуется класс B. Однако, класс B не содержит никаких полей. Если я создам массив элементов B, но использую его как массив элементов A, то будет ли это корректно?

В стандарте говорится, что это разрешено только с подобными типами:

When an expression that has integral type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the expression P points to element x[i] of an array object x with n elements,86 the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x [ i + j ] if 0 ≤ i + j ≤ n ; otherwise, the behavior is undefined. Likewise, the expression P - J points to the (possibly-hypothetical) element x [ i − j ] if 0 ≤ i − j ≤ n ; otherwise, the behavior is undefined.

Подобность типов описана рядом, но я не могу понять, являются ли в моём случае типы подобными. Какие именно типы считаются подобными?

Вот пример кода: https://ideone.com/ncRepZ https://ideone.com/nMvJ0r
Содержит ли он неопределённое поведение?

#include <iostream>

using namespace std;

struct A
{
  int x;
  A(int x) : x(x) {}
  virtual ~A() {}
};

struct B : A
{
  B() : A(7) {}
};

int main()
{
  A *a = new B[4];

  for (size_t q=0; q<4; ++q)
    cout << q << ": " << a[q].x << endl;

  delete [] a;

  return 0;
}

В случае, если это всё-таки не разрешено, достаточно ли добавить проверку на равенство размеров этих двух типов https://ideone.com/iSkJk0

static_assert(sizeof (A) == sizeof (B), "B must have same size as A");

чтобы гарантировать, что если программа компилируется, то она не содержит UB?

Qwertiy
  • 123,725

1 Answers1

3

По второй ссылке ясно написано, что типы подобны, если они совпадают с точностью до const и volatile, а также замены массива фиксированного размера на массив неизвестного размера:

Two types T1 and T2 are similar if they have cv-decompositions with the same n such that corresponding Pi components are either the same or one is “array of Ni” and the other is “array of unknown bound of”, and the types denoted by U are the same.

Преобразования между наследником и базовым типом в отношение подобия не входят.

Так что формально так делать нельзя, будет UB. На практике проявиться это UB может так же, как и любое другое нарушение strict aliasing rule:

A *a = new B[4];
a[1].x = 5;

B b = (B)a; (b+1)->x = 6;

std::cout << a[1].x << std::endl; // Слишком умный компилятор выведет 5

Pavel Mayorov
  • 58,537
  • Впрочем, в приведённом примере и всех его вариациях, которые я смог придумать (убрать присваивание b[1].x, поменять индекс, убрать оба присваивания a[1].x и b[1].x), gcc вообще сносит создание массива и инлайнит правильную константу (5, 6 или 7). Clang ведёт себя так же, но в случае с удалением обоих присваиваний оставляет создание массива. VS делает топорнее, но вроде не косячит, хотя подозреваю, его проще всего заставить сделать что-то не то в подобном коде... – Qwertiy Sep 11 '20 at 10:19
  • @Qwertiy проще всего заставить сделать "что-то не то" (но строго в рамках стандарта) как раз clang – Pavel Mayorov Sep 11 '20 at 10:28
  • Но он всё заинлайнил. А вот VS иногда делает mov ecx, DWORD PTR [rax+4] (хотя в большинстве случаев там тоже инлайн, но создание и инициализация массива остаётся) - думаю, на основе этого проще получить косяк? – Qwertiy Sep 11 '20 at 10:30
  • @Qwertiy всё строго наоборот. Пока компилятор "глупый" и работает в режиме "высокоуровнего ассемблера" - вы можете использовать всяких кулхаки без оглядки на стандарт. Пока типы A и B бинарно совместимы - ничего не поломается. А вот как только компилятор начинает заниматься оптимизацией - стандарт языка резко становится актуален. – Pavel Mayorov Sep 11 '20 at 12:20
  • @Qwertiy к примеру, в приведенном мною коде компилятор может доказать что указатели a+1 и b+1 ссылаются на разные объекты, а значит на место a[1].x можно заинлайнить 5. То, что он не стал доказывать такую сложную теорему, а просто заинлайнил 6 - это просто повезло. – Pavel Mayorov Sep 11 '20 at 12:21
  • 1
    Есть код: int a[2]; char* p = reinterpret_cast<char*>(&a[0]); p = p + 1. p указывает на char, который not similar типу int, следовательно выражение p + 1 является UB? – wololo Sep 11 '20 at 13:13
  • @wololo, нет, char явно прописан в исключениях - к нему кастить можно. – Qwertiy Sep 11 '20 at 13:16
  • 1
    @Qwertiy, так я спрашиваю не про каст, а про наращивание полученного указателя. Что-то в expr.add/6 char не указан в качестве исключения. – wololo Sep 11 '20 at 13:19