Изменение 1-мерного массива на многомерный массив

Принимая во внимание весь стандарт С++ 11, возможно ли, что любая соответствующая реализация преуспеет в первом утверждении ниже, но не имеет последнего?

#include <cassert>

int main(int, char**)
{  
    const int I = 5, J = 4, K = 3;
    const int N = I * J * K;

    int arr1d[N] = {0};
    int (&arr3d)[I][J][K] = reinterpret_cast<int (&)[I][J][K]>(arr1d);
    assert(static_cast<void*>(arr1d) ==
           static_cast<void*>(arr3d)); // is this necessary?

    arr3d[3][2][1] = 1;
    assert(arr1d[3 * (J * K) + 2 * K + 1] == 1); // UB?
}

Если нет, это технически UB или нет, и этот ответ меняется, если первое утверждение удалено (есть reinterpret_cast, чтобы сохранить здесь адреса?)? Кроме того, что, если преобразование выполняется в противоположном направлении (от 3d до 1d) или от массива 6x35 до массива 10x21?

EDIT:. Если ответ заключается в том, что это UB из-за reinterpret_cast, существует ли какой-то другой строго совместимый способ перестройки (например, через static_cast в/из промежуточного void *)?

Ответы

Ответ 1

reinterpret_cast ссылок

В стандарте указано, что lvalue типа T1 может быть reinterpret_cast ссылкой на T2, если указатель на T1 может быть reinterpret_cast на указатель на T2 (§5.2.10/11):

Выражение lvalue типа T1 может быть передано типу "ссылка на T2", если выражение типа "указатель на T1" может быть явно преобразовано в тип "указатель на T2", используя reinterpret_cast.

Итак, нам нужно определить, можно ли преобразовать a int(*)[N] в int(*)[I][J][K].

reinterpret_cast указателей

Указатель на T1 может быть reinterpret_cast указателем на T2, если оба T1 и T2 являются стандартными типами макета, а T2 не имеет более строгих требований к выравниванию, чем T1 (§ 5.2.10/7):

Когда prvalue v типа "указатель на T1" преобразуется в тип "указатель на cv T2", результатом является static_cast<cv T2*>(static_cast<cv void*>(v)), если оба T1 и T2 являются стандартными типами макета (3.9) и требования к выравниванию T2 не являются более строгими, чем требования T1, или если любой тип недействителен.

  • Являются ли int[N] и int[I][J][K] стандартными макетами?

    int является скалярным типом, а массивы скалярных типов считаются стандартными типами макета (§3.9/9).

    Скалярные типы, типы классов стандартного макета (раздел 9), массивы таких типов и cv-квалифицированные версии этих типов (3.9.3) совместно называются типами стандартного макета.

  • У int[I][J][K] нет более строгих требований к выравниванию, чем int[N].

    Результат оператора alignof дает требование согласования полного типа объекта (§3.11/2).

    Результат оператора alignof отражает требование выравнивания типа в случае полного объекта.

    Так как два массива здесь не являются подобъектами любого другого объекта, они являются полными объектами. Применение alignof к массиву дает требование выравнивания типа элемента (§5.3.6/3):

    Когда alignof применяется к типу массива, результатом будет выравнивание типа элемента.

    Таким образом, оба типа массива имеют одинаковое требование выравнивания.

Это делает reinterpret_cast действительным и эквивалентным:

int (&arr3d)[I][J][K] = *reinterpret_cast<int (*)[I][J][K]>(&arr1d);

где * и & - встроенные операторы, которые тогда эквивалентны:

int (&arr3d)[I][J][K] = *static_cast<int (*)[I][J][K]>(static_cast<void*>(&arr1d));

static_cast через void*

static_cast до void* допускается стандартными преобразованиями (§4.10/2):

Указатель типа "указатель на cv T", где T - тип объекта, может быть преобразован в prvalue типа "указатель на cv void". Результат преобразования "указателя на cv T" в "указатель на cv void" указывает на начало места хранения, где находится объект типа T, как если бы объект был наиболее производным объектом (1.8 ) типа T (т.е. не подобъект базового класса).

Затем разрешается static_cast до int(*)[I][J][K] (§5.2.9/13):

Значение типа "указатель на cv1 void" может быть преобразовано в prvalue типа "указатель на cv2 T", где T - тип объекта, а cv2 - это такая же cv-квалификация, как, или больше cv-квалификации, чем cv1.

Итак, отличное качество! Но можем ли мы получить доступ к объектам через ссылку на новый массив?

Доступ к элементам массива

Выполнение подтипирования массива в массиве типа arr3d[E2] эквивалентно *((E1)+(E2)) (§5.2.1/1). Пусть рассмотрим следующий подпиттинг массива:

arr3d[3][2][1]

Во-первых, arr3d[3] эквивалентно *((arr3d)+(3)). Lvalue arr3d претерпевает преобразование матрицы в указатель, чтобы дать int(*)[2][1]. Нет требования, чтобы базовый массив должен иметь правильный тип для этого преобразования. Затем получает значение указателей (что отлично в §3.10), а затем добавляется значение 3. Эта арифметика указателя также прекрасна (§5.7/5):

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

Этот указатель разыменован, чтобы дать int[2][1]. Этот процесс проходит один и тот же процесс для следующих двух индексов, что приводит к окончательному значению int l в соответствующем индексе массива. Это значение из-за результата * (§5.3.1/1):

Оператор унарного * выполняет косвенное обращение: выражение, к которому оно применяется, должно быть указателем на тип объекта или указателем на тип функции, а результатом является lvalue, относящееся к объекту или функции, к которой относится выражение.

Тогда отлично получается получить доступ к фактическому объекту int через это значение lvalue, потому что lvalue имеет тип int тоже (§3.10/10):

Если программа пытается получить доступ к сохраненному значению объекта с помощью glvalue, отличного от одного из следующих типов, поведение undefined:

  • динамический тип объекта
  • [...]

Так что, если я что-то пропустил. Я бы сказал, что эта программа четко определена.

Ответ 2

У меня создается впечатление, что это сработает. Вы выделяете один и тот же кусок смежной памяти. Я знаю, что стандарт C гарантирует, что он будет как минимум смежным. Я не знаю, что сказано в стандарте С++ 11.

Однако первое утверждение всегда должно быть истинным. Адрес первого элемента массива всегда будет одинаковым. Весь адрес памяти будет таким же, поскольку выделена одна и та же часть памяти.

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