Comments 49
Многомерные массивы ГАРАНТИРОВАННО лежат в памяти последовательно. Проблемы лежат исключительно в области гарантий языка.
Да, стандарт гарантирует, что элементы массива лежат последовательно, так же, как и гарантирует неопределенное поведение при выходе за границы массива.
Почему в PVS-Studio никто из экспертов по C/C++ не вычитывает и не проводит review ваших статей перед публикацией?
Похоже, в статье была какая-то ошибка, которую исправили, и первые комменты теперь выглядят странно?
Нет, в статье до сих пор написано:
Вернёмся к примеру с двумерным массивом
a[2][4]
. Результатом первого оператора индексированияa[0]
будет указатель наint[4]
, а значит доступ может осуществляться только в интервале[0...3]
.
Я понимаю, что массивы — это массовая слабая область знаний у очень многих.
Но коль пишется статья, почему бы не вычистить её от таких ошибок, дав на вычитку искушённым людям, дабы не распространять заблуждения?
Результатом первого оператора индексирования a[0]
будет отнюдь не указатель на int[4]
.
И далее здесь в комментариях есть даже ссылка на ответ ИИ по этому поводу.
Почему? Разве не может, условно, быть так?:
int array1[], garb, array2[], garb2, array3[], garb3;
int twod_array[] { array1, array2, array3 };
Потому что в стандарте в [dcl.array] написано про непрерывность
надо включить опции в кланге! желательно последней версии ) -std=c++2(3-6) -Wall -Wpedantic -fsanitize=undefined возможно какието еще и не отключать отображение ворнингов внимательно смотреть ворнинги и при этих опциях будет показывать выход за границы, но с -fsanitize=undefined, я еще не смотрел 3д проект свой возможно он даже не скомпилируется и улетит в андефайнед, на простых примерах мне показывал в годболте выходы за границыwarning
2мерный массив в одномерном почемуто пошустрее работает например с вокселями, там еще третье направление появляется просто по кубу кубической сеткой обходим в условиях плоского массива таким образом мы всегда в 1регионе памяти (альтернатива этому 3 мерный массив)
Результатом первого оператора индексирования
a[0]
будет указатель наint[4]
Конечно же, нет.
Тип выражения a[0]
отличается от указателя на int[4]
.
printf(" %d", a[0][i]);
Здесь двумерный массив
a
используется как одномерный: в цикле мы обращаемся к элементам черезa[0][i]
, гдеi
пробегает от0
доROWS * COLS
. А что тут неправильного?
Неправильным здесь является способ использования многомерного массива как одномерного.
Ведь согласно стандарту, все элементы массива располагаются в памяти последовательно
Именно поэтому следует получить адрес первого элемента и проиндексировать его:
printf(" %d", i[&**a]);
Если этот код непонятен, можно попроще:
printf(" %d", (&**a)[i]);
Если и этот непонятен, можно ещё проще:
printf(" %d", (&a[0][0])[i]);
В последнем варианте явным образом видно, что сначала был взят адрес первого элемента, который был тут же проиндексирован — ровно то, о чём я написал перед первым примером.
Ни один компилятор — не то, что не ругался на UB, но даже и не пикнул: все компиляторы сгенерировали правильный код, который отработал и напечатал ожидаемую последовательность.
Это — результат правильного способа использования многомерного массива как одномерного.
Ни один компилятор — не то, что не ругался на UB, но даже и не пикнул: все компиляторы сгенерировали правильный код, который отработал и напечатал ожидаемую последовательность.
Это — результат правильного способа использования многомерного массива как одномерного.
Как-то я все-же не уверен насчет "правильного способа". Описание работы built-in subscript operator с cppreference:
The built-in subscript expression E1[E2] is exactly identical to the expression *(E1 + E2) except for its value category (see below) and evaluation order(since C++17): the pointer operand (which may be a result of array-to-pointer conversion, and which must point to an element of some array or one past the end) is adjusted to point to another element of the same array, following the rules of pointer arithmetic, and is then dereferenced.
OK, смотрим Pointer arithmetic оттуда же:
When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.
[...]
Otherwise, if P points to the ith element of an array object x with n elements, given the value of J as j, P is added or subtracted as follows:
The expressions P + J and J + P
point to the i+jth element of x if i + j is in [0, n), and
are pointers past the end of the last element of x if i + j is n.
The expression P - J
points to the i-jth element of x if i - j is in [0, n), and
is a pointer past the end of the last element of x if i - j is n.
Other j values result in undefined behavior.
Проблема тут на мой взгляд в том, что в случае массива a[2][4]
из статьи результатом выражения &a[0][0]
является указатель, указывающий на первый элемент массива из четырех элементов (а не восьми), соответственно n
из цитаты будет 4, а не 8. Даже если все компиляторы сейчас для вашего трюка генерируют правильный код, это отнюдь не означает, что так будет всегда. В общем, я бы такие сомнительные на мой взгляд преобразования все равно не делал.
Для C++, видимо, действительно, необходимо преобразование:
std::printf(" %d", reinterpret_cast<int(&)[8]>(a)[i]);
Не уверен, требуется ли здесь "отмывание" с помощью std::launder
, ибо эта функция как будто специально описана так непонятно, что без тайного знания понять невозможно.
Для C, возможно, тип тоже должен приводиться (если разыменование и сразу же взятие адреса &x[0]
для C не "отвязывает" указатель от подобъекта в том смысле, что "отвязанным" указателем можно "гулять" по всему объекту):
printf(" %d", (*(int(*)[8])&a)[i];
Но, в любом случае, это не должно быть явное a[0][i]
, где нарушение границ налицо.
Ни один компилятор — не то, что не ругался на UB, но даже и не пикнул: все компиляторы сгенерировали правильный код, который отработал и напечатал ожидаемую последовательность.
Если подобные трюки делать в compile time, компилятор очень даже пикает.
Ну, constexpr
— это особая ипостась, преобразовать не даёт.
Через reinterpret_cast
— потому что становится не constexpr
, а через двойной static_cast
через void *
— сначала потому что "cast from 'void *' is not allowed in a constant expression in C++ standards before C++2c", а потом, после включения C++2c, — потому что "cast from 'void *' is not allowed in a constant expression because the pointed object type 'int[2][4]' is not similar to the target type 'int[8]'".
Наверное, следует подождать, когда reinterpret_cast
станет доступен в constexpr
-функциях.
Я модифицировал ваш пример:
constexpr int foo()
{
int arr[2][4]{{1, 2, 3, 4}, {5, 6, 7, 8}};
int *p0{arr[0]};
int res{0};
for (int i = 0; i < 5; ++i) {
res += *p0++;
}
return res;
}
static_assert(foo());
int main()
{}
Получается "read of dereferenced one-past-the-end pointer is not allowed in a constant expression".
Тогда я добавил ещё один указатель и очень интересный if
:
constexpr int foo()
{
int arr[2][4]{{1, 2, 3, 4}, {5, 6, 7, 8}};
int *p0{arr[0]};
int *p1{arr[1]};
int res{0};
for (int i = 0; i < 5; ++i) {
if (p0 == p1) {
p0 = p1;
}
res += *p0++;
}
return res;
}
static_assert(foo());
int main()
{}
Этот if
- бессмысленный, потому что выполнение его тела ничего не меняет:
if (p0 == p1) {
p0 = p1;
}
Однако, без него наблюдается "one-past-the-end pointer is not allowed".
Здесь у меня возникло какое-то ощущение, что компиляторостроители перегнули палку в этом месте, борясь за пространство для оптимизаций.
Мне не удалось найти способа для constexpr
-функций получить чистое решение для линейного доступа ко всему объекту.
а потом, после включения C++2c, — потому что "cast from 'void *' is not allowed in a constant expression because the pointed object type 'int[2][4]' is not similar to the target type 'int[8]'".
Наверное, следует подождать, когда
reinterpret_cast
станет доступен вconstexpr
-функциях.
У reinterpret_cast
есть правила насчет того, когда получившийся указатель может быть разыменован, а когда - нет (и эти правила эквивалентны правилам для указателя, получившегося в результате каста одного указателя в другой через void*
, потому что это абсолютно эквивалентные операции). Вот это вот "is not allowed because the pointed object type is not similar to the target type" - это никак не изменится, даже если reinterpret_cast
разрешат в constexpr
. ИМХО все эти трюки просто-напросто опасны, не нужны и не стоят потраченного времени.
У
reinterpret_cast
есть правила насчет того, когда получившийся указатель может быть разыменован, а когда - нет (и эти правила эквивалентны правилам для указателя, получившегося в результате каста одного указателя в другой через void*, потому что это абсолютно эквивалентные операции).
Это верно в отсутствие std::launder
.
ИМХО все эти трюки просто-напросто опасны, не нужны и не стоят потраченного времени.
Думаю, что конкретно этот "трюк" ограничен избыточно.
Не уследили за компиляторописателями.
Хотя, — вот вариант, который обещает быть рабочим:
#include <memory>
constexpr int foo()
{
int arr[2][4]{{1, 2, 3, 4}, {5, 6, 7, 8}};
int (&ar)[8]{*std::start_lifetime_as_array<int[8]>(&arr)};
// int *ar{std::start_lifetime_as_array<int>(&arr, 8)};
int res{0};
for (int i = 0; i < 5; ++i) {
res += ar[i];
}
return res;
}
static_assert(foo());
int main()
{}
Говорят, что пока ещё ни один из компиляторов не поддерживает ни start_lifetime_as
, ни start_lifetime_as_array
.
Однако, само название намекает, что данные "трюки" настолько востребованы, что для них даже добавили специальные функции.
Это верно в отсутствие
std::launder
.
Нет. std::launder
не предназначен для волшебного превращения одного типа по адресу в другой тип. Рекомендую ознакомиться с примерами в соответствующей статье на cppreference, там есть например такой:
int x2[2][10];
auto p2 = std::launder(reinterpret_cast<int(*)[10]>(&x2[0][0]));
// Undefined behavior: x2[1] would be reachable through the resulting pointer to x2[0]
// but is not reachable from the source
Однако, само название намекает, что данные "трюки" настолько востребованы, что для них даже добавили специальные функции.
А еще это как бы намекает что одних трюков с реинтерпрет кастами и лаундерингом недостаточно. Не говоря уж о том, что start_lifetime_as
предназначены скорее немного для другого: например, есть у вас память, замапленная через mmap
из файла какого-нибудь, и вы хотите превратить ее в объект (или массив объектов), чтобы потом с ней структурированно работать.
Нет. std::launder не предназначен для волшебного превращения одного типа по адресу в другой тип.
Здесь важнее то, где он примени́м и как.
Рекомендую ознакомиться с примерами в соответствующей статье на cppreference, там есть например такой:
Я пытался ознакомиться с самой сутью, но мне это не удаётся сделать.
int x2[2][10];
auto p2 = std::launder(reinterpret_cast<int(*)[10]>(&x2[0][0]));
// Undefined behavior: x2[1] would be reachable through the resulting pointer to x2[0]
// but is not reachable from the source
Именно этот пример вызвал наибольшие затруднения.
Как x2[1]
может быть доступен через указатель на массив из 10 элементов?
А еще это как бы намекает что одних трюков с реинтерпрет кастами и лаундерингом недостаточно.
Скорее, это намекает на то, что сначала допустили какую-то ошибку, а теперь лихорадочно пытаются костылить, чтобы эту ошибку обойти.
Не говоря уж о том, что start_lifetime_as предназначены скорее немного для другого: например, есть у вас память, замапленная через mmap из файла какого-нибудь, и вы хотите превратить ее в объект (или массив объектов), чтобы потом с ней структурированно работать.
Предназначены они, по задумке, могут быть и для другого.
Важно, для чего они могут быть использованы в принципе.
В частности, если они годятся для решения обсуждаемого вопроса, то не имеет значения, для чего они были задуманы изначально.
Здесь важнее то, где он примени́м и как.
std::launder
применим только если по "отмываемому" адресу действительно уже существует объект типа T
. Об этом недвусмысленно написано в его описании по ссылке. Нельзя пытаться "отмывать" многомерный массив в одномерный, это не будет работать, потому что по соответствующему адресу находится многомерный массив, а не одномерный, и std::launder
для этого не предназначен.
Как
x2[1]
может быть доступен через указатель на массив из 10 элементов?
Там написано не про это. Там написано, что x2[1]
недоступно через &x2[0][0]
, а через приведенное сочетание reinterpret_cast
и std::launder
кто-то пытается притвориться, что доступно - ведь через int
(*)[10]
оно действительно должно быть доступно, но фактически в данном случае это не так. В некотором роде этот пример обратен вашей ситуации - тут одномерный массив (точнее, указатель на первый элемент одномерного массива) пытаются превратить в многомерный через std::launder
.
Скорее, это намекает на то, что сначала допустили какую-то ошибку, а теперь лихорадочно пытаются костылить, чтобы эту ошибку обойти.
Ни std::launder
, ни std::start_lifetime_as
не являются "костылями" для обхода воображаемой "проблемы" с превращением многомерных массивов в одномерные (ну, или наоборот).
В частности, если они годятся для решения обсуждаемого вопроса, то не имеет значения, для чего они были задуманы изначально.
Могу сразу сказать, что вряд ли годятся :) По той же причине, по которой в C++ нельзя обращаться к "неактивным" элементам union
- по одному адресу не могут одновременно "жить" (выражаясь казенным языком - быть within their lifetime) несколько объектов разных типов.
std::launder
применим только если по "отмываемому" адресу действительно уже существует объект типа T. Об этом недвусмысленно написано в его описании по ссылке. Нельзя пытаться "отмывать" многомерный массив в одномерный, это не будет работать, потому что по соответствующему адресу находится многомерный массив, а не одномерный, иstd::launder
для этого не предназначен.
В C/C++ нет многомерных массивов, зато есть массивы массивов.
И сам тип массив является "недотипом": один однотипный массив нельзя присвоить другому, в отличие от тех же структур, также массив нельзя инициализировать однотипным массивом.
В этом смысле объектов типа "массив" не существует.
Косвенно это можно обнаружить по тому, что квалификаторы const
/volatile
к самому массиву не применимы, только к типам самих элементов.
Опять же, — в отличие от тех же структур.
В чём же может быть отличие между "одномерным" и "многомерным" массивом, учитывая вышесказанное, а также, учитывая, что элементы лежат "плотно"?
В стандарте очень часто — головоломка, потому что не описывается, что лежит в основе, но, кажется, я начинаю догадываться, что они могли иметь ввиду, хотя это также может быть неполным пониманием.
int arr[2][4]{{1, 2, 3, 4}, {5, 6, 7, 8}};
int *p0{arr[0]};
int *p1{arr[1]};
int res{0};
for (int i = 0; i < 5; ++i) {
if (p0 == p1) {
p0 = p1;
}
res += *p0++;
}
Если считать, что должны поддерживаться архитектуры, в которых доступ к памяти защищён по типу сегментов в IA-32 и принять, что при инициализации p0
выше "выдаётся" указатель, который безопасно можно разыменовывать только как int
'ы, и — в пределах 4 int
'ов в памяти, считая от начала памяти, занимаемой arr[0]
, и двигать, но не разыменовывать на 5-ый, иначе возникнет исключение общей защиты памяти, то дальше получается следующее.
При инициализации p1
выдаётся аналогичный указатель, но начало "сегмента" находится по адресу arr[1]
.
Тогда сравнение p0 == p1
, в котором сравнивается только адресная составляющая, "срабатывает" как раз в тот момент, когда p0
ещё может иметь такое значение, но разыменовывать его уже нельзя.
При присваивании в теле if
'а в p0
записывается полная информация из p1
: не только информация об адресе, которая в этот момент совпадает, но и информация о том, какой сегмент памяти доступен без срабатывания общей защиты памяти.
И поэтому при наличии этого if
'а дальнейшие итерации в цикле не приводят к срабатыванию этой защиты, то есть, UB, а без него однозначно привели бы.
Если продолжать рассуждать в этом смысле, то следующий код, по идее:
int (*ar)[8]{std::start_lifetime_as_array<int[8]>(&arr)};
должен создать указатель с адресом, равным адресу, по которому лежит исходный массив arr
, доступом к объектам типа int
и защитой в виде "сегмента", настроенного на все 8 элементов исходного массива.
И тогда проблем с одномерным доступом к всему массиву не возникает.
Но они не говорят, как всё устроено, и что за всеми этими штуками стои́т.
Там написано не про это.
Про это там тоже написано.
Там написано, что
x2[1]
недоступно через&x2[0][0]
, а через приведенное сочетаниеreinterpret_cast
иstd::launder
кто-то пытается притвориться, что доступно - ведь черезint
(*)[10]
оно действительно должно быть доступно, но фактически в данном случае это не так.
То есть, они неявно полагают, что можно адресную арифметику здесь применить, и на этом основании утверждать, что доступно?
Если снять одно измерение и повторить рассуждения, то получается следующее:
int x2[2];
auto p2 = std::launder(reinterpret_cast<int *>(&x2[0]));
// Undefined behavior: x2[1] would be reachable through the resulting pointer to x2[0]
// but is not reachable from the source
Или так — можно:
int x2[2];
int *p2 = &x2[0];
А так — UB?
int x2[2];
auto p2 = std::launder(reinterpret_cast<int *>(&x2[0]));
Без знания подоплёки не понять.
В этом смысле объектов типа "массив" не существует. Косвенно это можно обнаружить по тому, что квалификаторы const/volatile к самому массиву не применимы, только к типам самих элементов.
Ну, это натянутое утверждение. Для типов "массив" любой вложенности можно создавать алиасы через тот же using
, существует тип "указатель на массив", decltype
тоже прекрасно в курсе, что там за массив в конкретном случае и какие размерности он имеет. У компилятора нет никаких проблем отличить один массив от другого, коль скоро у него есть доступ к соответствующей декларации.
Но они не говорят, как всё устроено, и что за всеми этими штуками стои́т.
Вы слишком много внимания уделяете конкретной гипотетической реализации. Я же воспринимаю стандарты как описание некоей виртуальной машины, в котором написано, что эта виртуальная машина гарантированно может/должна прожевать (с тем или иным результатом), и на что не дается никаких гарантий.
Если снять одно измерение и повторить рассуждения, то получается следующее:
Ну вообще-то не получается, ибо в данном случае, имея массив типа int[2]
и указатель на первый элемент этого массива, есть легальный способ получить доступ к любому из его элементов. А с многомерными массивами (или, если угодно, массивами массивов), имея лишь указатель на первый элемент первого массива, нет легального способа получить доступ к элементам остальных массивов (требование "every byte that would be reachable through the result is reachable through p" не выполняется).
Ну, это натянутое утверждение. Для типов "массив" любой вложенности можно создавать алиасы через тот же using, существует тип "указатель на массив", decltype тоже прекрасно в курсе, что там за массив в конкретном случае и какие размерности он имеет. У компилятора нет никаких проблем отличить один массив от другого, коль скоро у него есть доступ к соответствующей декларации.
Верно.
Одновременно с этим у массива, в отличие от структуры, нет своей const
/volatile
-ности, значение массива, в отличие от значения структуры, нельзя присвоить значению однотипного массива, а также нельзя инициализировать.
Как следствие, в отличие от структуры, его нельзя по значению ни передать в функцию, ни вернуть оттуда.
При этом, что интересно, в составе структуры он прекрасно может быть использован во всех этих качествах, но — только от имени структуры.
Именно поэтому массив — это не "нетип", а именно "недотип".
Вы слишком много внимания уделяете конкретной гипотетической реализации.
Это — для того, чтобы иметь возможность дать ответ в общем случае, а не гадать.
Я же воспринимаю стандарты как описание некоей виртуальной машины, в котором написано, что эта виртуальная машина гарантированно может/должна прожевать (с тем или иным результатом), и на что не дается никаких гарантий.
Без описания того, как всё устроено, нельзя дать ответ в общем случае.
имея лишь указатель на первый элемент первого массива, нет легального способа получить доступ к элементам остальных массивов (требование "every byte that would be reachable through the result is reachable through p" не выполняется).
Хорошо, я изменю оригинальный пример с cppreference, создав массив не из 2-х подмассивов, а из одного:
int x2[1][10];
auto p2 = std::launder(reinterpret_cast<int(*)[10]>(&x2[0][0]));
Теперь, по логике того объяснения, которое было в оригинальном примере, здесь UB нет, верно?
Теперь, по логике того объяснения, которое было в оригинальном примере, здесь UB нет, верно?
По идее, да. Смотрим:
every byte that would be reachable through the result is reachable through
p
(bytes are reachable through a pointer that points to an objecty
if those bytes are within the storage of an objectz
that is pointer-interconvertible withy
, or within the immediately enclosing array of whichz
is an element).
p
в данном случае указывает на объект типа int
, который находится внутри immediately enclosing array типа int[10]
(то есть все элементы этого массива доступны через адресную арифметику), который в свою очередь занимает все пространство x2
, т.е. любой байт внутри x2
достижим через этот p
, так же как он достижим и через int
(*)[10]
.
Значит, для C++ единственный выход — использовать функцию конверсии.
На C++:
extern "C" int (*fun(int (*const p)[2][4]))[8];
Отдельно на C:
int (*fun(int (*const p)[2][4]))[8] {
return (int (*)[8])p;
}
потом с'link'овать вместе, вызывать из C++.
Вот здесь очень с умолчанием написано:
Any pointer to object can be cast to any other pointer to object. If the value is not correctly aligned for the target type, the behavior is undefined. Otherwise, if the value is converted back to the original type, it compares equal to the original value. If a pointer to object is cast to pointer to any character type, the result points at the lowest byte of the object and may be incremented up to sizeof the target type (in other words, can be used to examine object representation or to make a copy via memcpy or memmove).
Непонятно насчёт разыменования, но, поскольку для pointer to any character type явно подразумевается разыменование, хотя явным образом упорно об этом умалчивается, то и для преобразованного pointer to object логично ожидать разрешения на разыменование, лишь бы выравнивание было корректным.
Глупости не пишите, выражение вида &* само себя нейтрализует. Массивы сводятся до (a+I)
Массивы сводятся до (a+I)
Говорит ли применённое мной выражение i[&**a]
о том, что я этого не понимаю?
выражение вида &* само себя нейтрализует.
Я проверил один пример на pure C:
#include <stdlib.h>
#include <stdio.h>
int main(void) {
char a[5];
printf("%zu\n", sizeof a);
printf("%zu\n", sizeof &*a);
return EXIT_SUCCESS;
}
Печатается:
5
8
Какая-то некачественная выходит нейтрализация.
Глупости не пишите
С этого момента попрошу поподробнее.
Не надо себя закапывать еще дальше.
Во первых:
n1256::
6.3.2
3. Except when it is the operand of the sizeof operator or the unary & operator,or isa string literal used to initialize an array, anexpression that has type `array of type'' is converted to an expression with type
`pointer to type'' that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.
6.5.3.1
3. The unary & operator yields the address of its operand. If the operand has type `type'' the result has type
`pointer to type''. I , f the operand is the result of a unary operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted, except that the constraints on the operators still apply and the result is not an lvalue. Similarly, ifthe operand is the result of a [] operator,neither the & operator nor the unary that is implied by the [] is evaluated and the result is as if the & operator were removedand the [] operator were changed to a + operator. Otherwise, the result is apointer to the object or function designated by its operand.
В вашем примере, char a[5] имеет тип: array-type, и при применении с оператором sizeof не "разлагается" в (a+5), по итогу получаем размер массива.
Во втором случае: выражение вида sizeof &*a можно свести (что и будет сделано) до sizeof a. a - имеет тип 'pointer to' и на вашей (х64) системе, указатели имеют размер 8, что и было продемонстрировано.
В третьих, ваши примеры изобилуют множеством символов, которые, буквально, не участвуют в выражениях, и вся операция "прохода по многомерному массиву как одномерному", можно представить в виде *(*(a+0)+i).
Так же можно свести a+0 (Разыменование указателя типа "type" (*)[] до "type" *, чтобы арифметика работала, для нашего случае, правильно) до просто a, но, на мой взгляд, так яснее.
Не надо себя закапывать еще дальше.
Кому не надо?
Поскольку вы можете отвечать только за себя, то, видимо, вам.
В вашем примере, char a[5] имеет тип: array-type, и при применении с оператором sizeof не "разлагается" в (a+5), по итогу получаем размер массива.
Операция sizeof
применима к массиву.
Поэтому неявное приведение array-to-pointer conversion не выполняется.
Во втором случае: выражение вида sizeof &*a можно свести (что и будет сделано) до sizeof a. a - имеет тип 'pointer to' и на вашей (х64) системе, указатели имеют размер 8, что и было продемонстрировано.
Во втором случае операция *
не применима к массиву, поэтому выполняется неявное приведение array-to-pointer conversion.
Вы говорили:
выражение вида &* само себя нейтрализует.
Как видите, — есть нюансы, не позволяющие так утверждать в общем случае.
И комбинация операций &*
была применена мной сознательно как раз специально для того, чтобы вызвать array-to-pointer conversion.
Я полагал, что это полностью "отвязывает" элемент от массива в том смысле, что этим указателем на первый элемент можно пользоваться для адресации всей памяти, занимаемой массивом массивов, но это оказалось не так, — не "отвязывает", несмотря на array-to-pointer conversion.
В третьих, ваши примеры изобилуют множеством символов, которые, буквально, не участвуют в выражениях, и вся операция "прохода по многомерному массиву как одномерному", можно представить в виде ((a+0)+i).Так же можно свести a+0 (Разыменование указателя типа "type" (*)[] до "type" *, чтобы арифметика работала, для нашего случае, правильно) до просто a, но, на мой взгляд, так яснее.
Как я уже написал выше, комбинация операций &*
была применена мной сознательно как раз специально для того, чтобы вызвать array-to-pointer conversion.
Именно поэтому она там не "лишняя".
Вы так и не ответили на вопрос:
Говорит ли применённое мной выражение
i[&**a]
о том, что я этого не понимаю?
заданный в ответ на ваше:
Массивы сводятся до (a+I)
И про глупости так и не написали.
В переписке с @KanuTaH мне стало понятно, в чём я заблуждался.
Вы же, похоже, пытаетесь оказаться правым за счёт того, что я окажусь неправым.
Не сто́ит пытаться.
Чукча не читатель, чукча писатель.
Не надо ничего "вызывать". В моем посте есть выдержка из стандарта. где черному по белому написано: "Если идентификатор array type не используется с sizeof или & - то тип его конвертируется до pointer to type.
Все! Не надо ничего как дождь "вызывать". Оно само, без вас, отлично справится.
"Во втором случае операция *
не применима к массиву, поэтому выполняется неявное приведение array-to-pointer conversion. "
Этот отрывок я прочитал, но понять, что имелось, вообще, в виду, не сумел.
Не надо ничего "вызывать".
Кому не надо?
В моем посте есть выдержка из стандарта. где черному по белому написано: "Если идентификатор array type не используется с sizeof или & - то тип его конвертируется до pointer to type.
Именно поэтому я специально и сознательно добавил пару операций &*
, чтобы происходило array-to-pointer conversion.
И факту этому строго наплевать, кому там и что надо.
Array-to-pointer conversion там происходит.
Все! Не надо ничего как дождь "вызывать".
Кому не надо?
Имеется факт, что у меня используется пара операций &*
.
Имеется факт, что это приводит к array-to-pointer conversion.
Сообщаю вам, что эта пара операций была добавлена мной сознательно, специально для того, чтобы имел место факт array-to-pointer conversion.
В написанном выше абзаце приводятся факты, там никому ничего не надо.
Оно само, без вас, отлично справится.
Оно "справится" только в случае применения пары операций &*
.
Без меня эта пара операций там не появится.
Ведь именно я эту пару туда вписал.
Если бы я эту пару операций туда не вписал, оно бы не "справилось".
Тип выражения &**a
, со вписанной мной парой &*, есть указатель на int
.
Тип выражения *a
есть массив из 4-х int
'ов.
С этими фактами вы не будете спорить?
Без вписанной мной пары операций &*
array-to-pointer conversion не происходит, то есть, без этой пары оно не "справляется": вместо указателя на int
выражение *a
без использования пары &*
имеет тип массив из 4-х int
'ов.
А с этими фактами спорить не будете?
Я не знаю, как ещё объяснить очевидное.
Без вписанной мной пары операций &*
array-to-pointer conversion не происходит, то есть, без этой пары оно не "справляется": вместо указателя на int
выражение a
без использования пары &
имеет тип массив из 4-х int
'ов.
До того как писать такие утверждения, можно было бы, за более чем неделю, попытаться скомпилировать это, нет? Чтобы глупости не писать.
int a[][2] <-- Это, указатель на массив из [2] int
*a - Это, указатель на int.
В этом можно было бы легко убедиться, попытавшись присвоить 'a' или же '*a" к
int* ptr и посмотреть, что получится.
Храни вас Господь, да поможет вам AI.
Вы пытались процитировать меня:
Без вписанной мной пары операций
&*
array-to-pointer conversion не происходит, то есть, без этой пары оно не "справляется": вместо указателя наint
выражениеa
без использования пары&
имеет тип массив из 4-хint
'ов.
И затем ответили на эту цитату:
До того как писать такие утверждения, можно было бы, за более чем неделю, попытаться скомпилировать это, нет?
Очевидно, я не пытался, а именно компилировал и на эту тему вам пример уже показывал.
Чтобы глупости не писать.
Вы не потрудились обосновать.
А я все свои утверждения обосновываю, если вы ещё не заметили.
int a[][2] <-- Это, указатель на массив из [2] int
Нет, это не указатель на массив из 2 int
'ов.
Очевидно, что вы не пытались это компилировать, иначе компиляторы бы вам английским языком разъяснили, что это — массив.
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int a[][2];
return EXIT_SUCCESS;
}
Например, здесь видно, как gcc ясно пишет:
<source>:5:13: error: array size missing in 'a'
5 | int a[][2];
А clang пишет ещё яснее:
source>:5:6: error: definition of variable with array type needs an explicit size or an initializer 5 | int a[][2];
definition of variable with array type — куда уж яснее?
И кто здесь после этого пишет глупости?
И чтобы это стало ещё очевиднее, приведу пример с исходным массивом:
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int a[2][4];
printf("sizeof *a: %zu\n", sizeof *a);
printf("sizeof &**a: %zu\n", sizeof &**a);
return EXIT_SUCCESS;
}
Печатается следующее:
sizeof *a: 16
sizeof &**a: 8
Очевидно, что *a
— массив из 4-х int'ов.
А &**a
— указатель на int.
В том, что первое — массив, а второе — указатель, можно дополнительно убедиться, заменив 4 в массиве на другое число и обнаружив, что размер *a
изменится соответственно, а размер &**a
останется неизменным.
*a - Это, указатель на int.
Конечно же, нет.
В этом можно было бы легко убедиться, попытавшись присвоить 'a' или же '*a" кint* ptr и посмотреть, что получится.
То, что при этом произойдёт array-to-pointer conversion, вас, конечно же, не интересует.
Храни вас Господь, да поможет вам AI.
Не мне, а — вам, почитайте ответ AI на этот вопрос.
"Тут была тирада на тему, что никаких массивов не существует, и это все абстракция над указателем, и вообще, sizeof специальный оператор компиляции, бла-бла-бла"
А потом я понял, вы просто не в курсе, что "&" и "*" имеют правую ассоциативность и поэтому продолжаете писать свои глупости.
Суть то в чем? Надо писать &*? Нет, не надо.
"Тут была тирада на тему, что никаких массивов не существует, и это все абстракция над указателем, и вообще,
У кого была тирада?
Уже и массивов не существует?
sizeof специальный оператор компиляции, бла-бла-бла"
Попробуйте во время компиляции повычислять sizeof
VLA.
А потом я понял, вы просто не в курсе, что "&" и "*" имеют правую ассоциативность и поэтому продолжаете писать свои глупости.
Ассоциативность имеет смысл только для бинарных и более арных операций.
Глупости пишете вы, и сразу о них забываете, как только я показываю вашу неправоту.
Почему-то вы "вдруг" напрочь забыли о своём утверждении:
*a - Это, указатель на int.
Теперь о какой-то ассоциативности пишете зачем-то.
Префиксные унарные операции всегда выполняются справа налево, в данном случае, сначала *
, затем &
.
Суть то в чем? Надо писать &*? Нет, не надо.
Кому надо?
Кому не надо?
Какое отношение чьё-то "надо/не надо" имеет к обсуждаемому вопросу?
Не поверил бы, что кто-то так делает, если бы не увидел самолично такое в коде какой-то старой игры, Quake или Doom (точно не помню). Видимо древние компиляторы Си допускали такие вольности, современные же ругаются.
А существует ли в C++ какая-либо легальная возможность работать с многомерными массивами как с одномерными или наоборот? Или любые поползновения в эту сторону сломают type-based alias analysis или какие-либо другие оптимизации?
идея использовать двумерный массив как одномерный
Странно. На МК я наоборот, чаще многомерный массив реализую в одномерном, так как в этом случае проще в коде контролировать выход за его пределы. Да и RAM экономится, которой всегда мало.
Интеловский фортран может не просто разместить элементы многомерного массива не подряд, но даже "перемешать" массивы в памяти, чтобы ускорить обращение к ним, когда несколько массивов затрагиваются в одном цикле. Не исключено, что их C тоже так делает при высоких оптимизациях. Однажды наступил на это, когда мне пришлось отправлять многомерный массив через MPI.
Зачем нужен этот UB если в тривиальных ситуациях получается лажа там, где простой С нормально(ожидаемо) бы отработал?
Для оптимизаций.
Видите, как здесь gcc мощно соптимизировал?
Всплывающее описание для инструкции процессора ud2
:
"Generates an invalid opcode exception. This instruction is provided for software testing to explicitly generate an invalid opcode exception. The opcodes for this instruction are reserved for this purpose."
Правда, я боюсь, что теперь вы будете ругаться ещё сильнее.
С точки зрения языка все верно и противоречий со здравым смыслом нет. Ничего удивительного.
Хочешь итерироваться как по одномерному - сделай кастование к одномерному массиву или просто к указателю.
простите ничего не надо кастовать, просто пользуетесь 1мерным массивом
Да-да, а что делать если массив приходит извне?
Или вам нужно работать с ним и в многомерном представлении и в одномерном.
Чтобы работать и в одномерном и многомерном, с возможностью переходить от одного к другому, лучше использовать std::mdspan
(или аналог, который будет работать в более старых стандартах).
Если извне приходит многомерный, то надежно сделать это без "обратного адаптирования"( с функцией вроде std::array<size_t, N> get_ndim_index(size_t)
) или побайтового копирования (std::memcpy
) вряд ли удастся
Ну вообще такая практика выглядит как из разряда грязных трюков. Если вдруг возникает такая необходимость, то почему не сделать union для 1- и 2-мерного массивов, и уже обращаться к ним в зависимости от ситуации без чёрной магии?
С union тоже нельзя. Стандарт прямо запрещает случаи использования, когда пишется значение одного типа, а читается значение другого типа.
С union тоже нельзя. Стандарт прямо запрещает случаи использования, когда пишется значение одного типа, а читается значение другого типа.
В C, начиная с C99, — можно:
If the member used to access the contents of a union is not the same as the member last used to store a value, the object representation of the value that was stored is reinterpreted as an object representation of the new type (this is known as type punning).
В C++ — да, нельзя.
Безопасная работа с массивами? Нет, не слышали