Индексирование многомерных массивов с использованием указателя на элементы

Насколько я знаю, многомерный массив в стеке будет занимать непрерывную память в порядке строк. Является ли поведение undefined индексировать многомерный массив, используя указатель на элементы в соответствии со стандартом ISO С++? Например:

#include <iostream>
#include <type_traits>
int main() {
  int a[5][4]{{1,2,3,4},{},{5,6,7,8}};
  constexpr auto sz = sizeof(a) / sizeof(std::remove_all_extents<decltype(a)>::type);
  int *p = &a[0][0];
  int i = p[11];  // <-- here
  p[19] = 20;  // <-- here
  for (int k = 0; k < sz; ++k)
    std::cout << p[k] << ' ';  // <-- and here
  return 0;
}

Выше код будет компилироваться и запускаться правильно, если указатель не выходит из границы массива a. Но происходит ли это из-за того, что поведение компилятора или язык стандартно? Любая ссылка из стандарта ISO С++ была бы лучше всего.

Ответ 1

Проблема заключается в строгом правиле aliasing, которое существует в моем проекте n3337 для С++ 11 в 3.10 Lvalues ​​и rvalues ​​[basic.lval] § 10. Это исчерпывающий список, который явно не допускает псевдоним многомерного массива до одномерного целого размера.

Итак, даже если действительно требуется, чтобы массивы последовательно выделялись в памяти, что доказывает, что размер многомерного массива, например, T arr[n][m], является произведением размерности по размеру элемента: n * m *sizeof(T), При преобразовании в указатели char вы даже можете выполнять операции арифметического указателя на весь массив, поскольку любой указатель на объект может быть преобразован в указатель char, а указатель char может использоваться для доступа к последовательным байтам объекта (*).

Но, к сожалению, для любого другого типа стандарт допускает только операции арифметического указателя внутри одного массива (и по определению разустановка элемента массива совпадает с как разыменование указателя после арифметики указателя: a[i] *(a + i)). Поэтому, если вы оба уважаете правило по арифметике указателей и строгому правилу псевдонимов, глобальная индексация многомерного массива не определяется стандартом С++ 11, если вы не пройдете через char указатель арифметики:

int a[3][4];
int *p = &a[0][0]; // perfectly defined
int b = p[3];      // ok you are in same row which means in same array
b = p[5];          // OUPS: you dereference past the declared array that builds first row

char *cq = (((char *) p) + 5 * sizeof(int)); // ok: char pointer arithmetics inside an object
int *q = (int *) cq; // ok because what lies there is an int object
b = *q;            // almost the same as p[5] but behaviour is defined

Арифметика указателя char наряду со страхом сломать много существующего кода объясняет, почему весь известный компилятор молча воспринимает наложение многомерного массива с 1D одного и того же глобального размера (он приводит к тому же внутреннему код), но технически глобальная арифметика указателей действительна только для указателей char.


(*) Стандарт объявляет в 1.7. Модель памяти С++ [intro.memory], которая

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

а затем в 3.9 Типы [basic.types] §2

Для любого объекта (кроме подобъекта базового класса) тривиально-скопируемого типа T, является ли объект имеет допустимое значение типа T, базовые байты, составляющие объект, могут быть скопированы в массив из char или без знака char.

и для их копирования вы должны получить к ним доступ через char * или unsigned char *

Ответ 2

Я считаю, что поведение в вашем примере технически undefined.

Стандарт не имеет понятия многомерного массива. То, что вы на самом деле объявили, представляет собой "массив из 5 массивов из 4 целых чисел". То есть a[0] и a[1] представляют собой фактически два разных массива из 4-х целых чисел, оба из которых содержатся в массиве a. Это означает, что a[0][0] и a[1][0] не являются элементами одного и того же массива.

[expr.add]/4 говорит следующее (акцент мой)

Когда выражение, которое имеет интегральный тип, добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива, а массив достаточно большой результат указывает на смещение элемента от исходного элемента, так что разница нижние индексы результирующих и исходных элементов массива равны интегральному выражению. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P) + N (что эквивалентно N + (P)) и (P) -N (где N имеет значение n) указывают соответственно на я + n-й и i-n-й элементы массива объект, если они существуют. Более того, если выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает один за последним элементом объекта массива, а если выражение Q точек один за последним элементом объекта массива, выражение (Q) -1 указывает на последний элемент массива объект. Если оба операнда указателя и результат указывают на элементы одного и того же объекта массива или одно прошлое последний элемент объекта массива, оценка не должна приводить к переполнению; в противном случае поведение undefined

Итак, поскольку p[11] расширяется до *(p + 11), и поскольку p и p + 11 не являются элементами одного и того же массива (один из них является элементом a[0], а другой - более одного элемента за конец a[0]), поведение этого сложения undefined.

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

Ответ 3

если вы объявите

int  arr[3][4][5];

тип arr - int[3][4][5], тип arr[3] - int[4][5] и т.д. Массив массива массивов, но НЕ массив указателей. Посмотрим, что произойдет, если мы увеличим первый индекс? Он сдвигал бы указатель вперед по размеру элемента массива, но элемент массива из arr был двумерным массивом! Это эквивалентно увеличению: arr + sizeof(int[4][5])/sizeof(int) или arr + 20.

Итерируя таким образом, мы обнаружим, что arr[a][b][c] равно *(*(*(arr + a) + b) + c), при условии, что никогда не будет заполнения с массивами (чтобы соответствовать обязательной совместимости типов POD с C99):

*((int*)arr + 20*a +  5*b + c)

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