Почему int * [] распадается на int **, но не int [] []?
Я пытаюсь понять природу распада типа. Например, все мы знаем, что в определенном контексте распад массивов в указатели. Моя попытка понять, как int[]
приравнивается к int*
, но как двумерные массивы не соответствуют ожидаемому типу указателя. Вот тестовый пример:
std::is_same<int*, std::decay<int[]>::type>::value; // true
Возвращает true, как ожидалось, но это не так:
std::is_same<int**, std::decay<int[][1]>::type>::value; // false
Почему это не так? Я, наконец, нашел способ заставить его возвращать true, и это было сделано при указании первого измерения указателем:
std::is_same<int**, std::decay<int*[]>::type>::value; // true
И утверждение верно для любого типа с указателями, но с последним, являющимся массивом. Например (int***[] == int****; // true
).
Могу ли я объяснить, почему это происходит? Почему типы массивов не соответствуют типам указателей, как и ожидалось?
Ответы
Ответ 1
Почему int*[]
распадается на int**
, но не int[][]
?
Потому что с ним было бы невозможно выполнить арифметику указателя.
Например, int p[5][4]
означает массив (длина-4 массив int
). Нет указателей, это просто смежный блок памяти размером 5*4*sizeof(int)
. Когда вы запрашиваете конкретный элемент, например. int a = p[i][j]
, компилятор действительно делает это:
char *tmp = (char *)p // Work in units of bytes (char)
+ i * sizeof(int[4]) // Offset for outer dimension (int[4] is a type)
+ j * sizeof(int); // Offset for inner dimension
int a = *(int *)tmp; // Back to the contained type, and dereference
Очевидно, что это может быть сделано только потому, что он знает размер "внутреннего" измерения (измерений). Передача в int (*)[4]
сохраняет эту информацию; это указатель на (длина-4 массив int
). Однако int **
не работает; это просто указатель на (указатель на int
).
Для получения дополнительной информации см. следующие разделы часто задаваемых вопросов C:
(Все это для C, но это поведение практически не изменяется в С++.)
Ответ 2
C не был "спроектирован" как язык; вместо этого функции были добавлены по мере необходимости, с целью не разорвать предыдущий код. Такой эволюционный подход был хорошим в те времена, когда C разрабатывался, поскольку это означало, что разработчики в большинстве случаев могли воспользоваться преимуществами более ранних улучшений в языке, прежде чем все, что может потребоваться для этого, было разработано. К сожалению, способ, которым развилась обработка массивов и указателей, привел к множеству правил, которые, в ретроспективе, неудачны.
На современном языке C существует довольно значительная система типов, а переменные имеют четко определенные типы, но вещи не всегда были такими. Объявление char arr[8]
; будет выделять 8 байтов в данной области и сделать arr
указывать на первую из них. Компилятор не знал бы, что arr
представляет массив - он будет представлять указатель char, как и любой другой char*
. Из того, что я понимаю, если бы кто-то объявил char arr1[8], arr2[8];
, утверждение arr1 = arr2;
было бы совершенно законным, будучи несколько эквивалентным концептуально char *st1 = "foo, *st2 = "bar"; st1 = st2;
, но почти всегда представляло бы ошибку.
Правило, которое массивы разлагаются на указатели, было связано с тем, что массивы и указатели действительно были одним и тем же. С тех пор массивы стали признаваться как особый тип, но язык должен был оставаться практически совместимым с теми днями, когда они не были. Когда правила формулируются, вопрос о том, как обрабатывать двумерные массивы, не является проблемой, потому что такого не было. Можно было бы сделать что-то вроде char foo[20]; char *bar[4]; int i; for (i=0; i<4; i++) bar[i] = foo + (i*5);
, а затем использовать bar[x][y]
так же, как теперь использовать двумерный массив, но компилятор не будет рассматривать вещи таким образом - он просто увидел bar
как указатель к указателю. Если бы кто-то хотел сделать foo [1] точкой где-то совершенно отличным от foo [2], можно было бы законно сделать это.
Когда два двумерных массива были добавлены в C, нет необходимости поддерживать совместимость с более ранним кодом, который объявлял двумерные массивы, потому что их не было. Хотя было бы возможно указать, что char bar[4][5];
будет генерировать код, эквивалентный тому, что было показано с помощью foo[20]
, и в этом случае a char[][]
можно было бы использовать как char**
, считалось, что так же, как присвоение переменные массива были бы ошибкой в 99% случаев, поэтому тоже было бы перераспределение строк массива, если бы это было законно. Таким образом, массивы в C распознаются как разные типы, с их собственными правилами, которые немного нечетны, но являются тем, чем они являются.
Ответ 3
Потому что int[M][N]
и int**
являются несовместимыми типами.
Однако int[M][N]
может распасться на int (*)[N]
. Итак, следующее:
std::is_same<int(*)[1], std::decay<int[1][1]>::type>::value;
должен предоставить вам true
.
Ответ 4
Двумерные массивы не сохраняются как указатель на указатели, а как непрерывный блок памяти.
Объект, объявленный как тип int[y][x]
, представляет собой блок размера sizeof(int) * x * y
, тогда как объект типа int **
является указателем на int*