Pull to refresh

Comments 170

Когда в универе учился подозрительно относился к обычным массивам. Они жутко неудобные и из плюсов остается только скорость работы. В итоге после собственных поисков и обсуждения с преподами также пришел к использованию std::array и std::vector

Тут, мне кажется, тоже ещё вопрос, а быстрее ли build-in массив, чем std::array? Учитывая, что последний, фактически, это тонкая прослойка над первым, даже этот плюс не факт, что перевешивает.

Да, быстрее, причём ощутимо. Писал backtracking, где миллиарды итераций, и все обёртки и абстракции оптимизировались в простые структуры, в основном массивы. Разница в рядовом приложении вряд ли будет ощутима, но там где большое количество повторений миллисекунда может превратится в лишний час, а то и не один, работы.

Использование std::array в большинстве случаев должно генерировать абсолютно такой же код, как и для сишного массива. На каких конкретно операциях вылезает разница?

Например, на передаче в функцию по значению.

Ну так по факту сишный массив по значению не передаётся - если нужна та же семантика, std::array надо передавать по ссылке (и в бинарном коде это будет тот же самый адрес первого элемента).

Вот да. Проблема может быть, если был typdef массива, а потом стал тайпдефаться в то же самое уже std::array. Получится неприятно.

Конечно нельзя просто так заменить одно на другое. Но если сразу сознательно использовать std::array, причин для просадки производительности не вижу.

Против практики не попрёшь, но...

И libstdc++, и libcxx в std::array в прямом смысле просто используют оператор индексации build-in массива в своём операторе индексации. Оно, скорее всего, заинлайнится. Поэтому мне пока что не очевидно, как std::array может проигрывать, во всяком случае в индексации. Да, там есть асерты даже в operator[], но они не должны отрабатывать в продакшн коде.

Инициализация и там и там агрегатов, так что тоже разницы не должно быть.

Почитал что такое backtracking и не очень понял, где использовался массив фикисрованной длины (т.е. std:array), да и собственно сколько в нем элементов было-то.

Если можно, поподробнее раскажите.

Плюс у него насамом деле в монотонности памяти и том что массив char может служить хранилищем любого объекта (по формулировке в стандарте)

В std::vector элементы также лежат в непрерывном виде.

Верно, и реализовпн через динамически выделенный массив (с закидоном на UB). но стандарт говорит о другом. По факту, обьект можно разместить в таком массиве (placement new) или инспектировать через него (на совести программиста и реализации)

А в чём закидон на UB? Если переаллокация сделается?

Формально большинство "оптимальных" реализаций вектора использовали malloc() для выделения памяти без инициализации.

https://stackoverflow.com/a/52997191/2742717

В реализации так МОЖНО делать, так как это сделано авторами компилятора (и возможно, компилятор имеет свой шаблон кода для обработки такого случая).

В 23м это было предложено следующее https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p0593r6.html

Да, спасибо, что подсветили. Тут к вопросу о том, возвращает ли malloc указатель на элемент массива? Если нет, то итерировать по этой памяти формально нельзя, даже если это - массив char.

И в std::array, и в std::vector память расположена последовательно. Если std::vector хотя бы может переаллоцировать память, то std::array - это просто обёртка на буфером, то есть массивом.

UFO just landed and posted this here

Классика) Причины понятны. Но я вот сам не встречал случаев, когда так записать и правда удобнее. Если у кого-то они есть, здорово бы было посмотреть.

Часто с литералами так ((n >> i)%16)["0123456789ABCDEF"] или похожие. у встроенного оператора чуть больше возможностей

Тут хочешь не хочешь, но спросишь - почему не отвести отдельную переменную под этот литерал? Но об удобстве точно не спорят, пример хороший, спасибо!

Но привычный порядок на два символа короче ):
"0123456789ABCDEF"[(n >> i)%16]

По идее, если литерал ДЛИННЫЙ, то будет удобнее, если в начале) Но почему тогда не отвести отдельную переменную?

Часто с литералами так ((n >> i)%16)["0123456789ABCDEF"] или похожие.

А что мешает записать так: "0123456789ABCDEF"[(n >> i) % 16]?
Как раз на лишнюю пару круглых скобок короче.

Но я вот сам не встречал случаев, когда так записать и правда удобнее. Если у кого-то они есть, здорово бы было посмотреть.

#include <cstdlib>
#include <iostream>

int main() {
	int const a[]{1, 2, 3};

	for (auto const *itr{a}, *const end{1[&a]}; itr != end; ++itr) {
		std::cout << *itr << std::endl;
	}

	return EXIT_SUCCESS;
}

Вариант для C:

#include <stdlib.h>
#include <stdio.h>

int main(void) {
	int const a[] = {1, 2, 3};

	for (int const *itr = a, *const end = 1[&a]; itr != end; ++itr) {
		printf("%i\n", *itr);
	}

	return EXIT_SUCCESS;
}

А вот так не понял. Как оно превращается в адрес конца? Я думал оно позволяет свапать индекс и имя переменной из-за ассоциативности.

Я думал оно позволяет свапать индекс и имя переменной из-за ассоциативности.

Да, в "обычном" виде пришлось бы использовать скобки, чтобы "преодолеть" приоритеты и заставить операции выполняться в нужном порядке: (&a)[1].

А вот так не понял. Как оно превращается в адрес конца?

Требуется понимание адресной арифметики и типов.

Преобразуем к независимому виду: *(&a + 1).
Приоритет & выше бинарного +.

Итак, раз берём адрес, то &a — указатель, но на элемент — какого типа?
Мы берём адрес массива, значит на массив.
На массив из 3-х элементов типа int.

Далее идёт адресная арифметика, поэтому жизненно необходимо знать sizeof элемента, к указателю на который применяется адресная арифметика, ибо указатель будет "шагать" именно на этот размер, и этот размер есть sizeof целого массива a, массива из 3-х элементов типа int.

К указателю прибавляется 1, значит указатель "шагнёт" на 1 такой массив.
И получится адрес сразу за концом массива.

После этого данный адрес разыменовывается.
Поскольку указатель — на массив, то, в результате разыменования получится массив (того же типа, что и массив a, то есть, из 3-х элементов типа int).

При попытке проинициализировать массивом переменную типа указатель на int с именем end, этот массив неявно приводится к указателю на свой первый элемент, который как раз тоже имеет тип int, поэтому никаких дополнительных приведений не требуется, точно так же, как этого не потребовалось при "инициализации массивом a" указателя itr.

Но адрес этого элемента как раз находится сразу за концом массива a, что и требовалось получить.

Для единообразия можно написать так:

	for (int const *itr = 0[&a], *const end = 1[&a]; itr != end; ++itr) {

Или, в более привычном виде, так:

	for (int const *itr = (&a)[0], *const end = (&a)[1]; itr != end; ++itr) {

Типы выражений a, (&a)[0] и (&a)[1] — одинаковы.
Это всё — массивы из 3-х элементов типа int.
И каждый из них может быть неявно приведён к указателю на свой первый элемент.

Теперь, думаю, всё должно быть полностью понятно.

Огонь! Сохранил себе, спасибо!

Если в плюсах можно (и нужно!) сказать, что лучше пользовать итераторы или ranged-for, то в С это выглядет вполне себе!

Огонь! Сохранил себе, спасибо!

Если вздумаете применять в коде, который потом отправляется на review, скорее всего, узнаете много новых слов.

Это больше для собственного развития и только для своего личного кода.

А я любознательный)

Сам я на Си не пишу, а подход в плюсах и так обозначил выше. Но для собственного развития пример и правда очень хороший! Может, даже будет хорошей идеей посоветовать коллегам как вопрос для интервью, чтобы проверить границу понимания приоритетов.

То что имя массива это адрес первого элемента - конечно же ошибка дизайна С/С++. Сейчас ее наверное не исправить, если только не пойти путем явного версионирования языка, что я периодически предлагаю в разных обсуждениях. В начале файла пишем #pragma version(2), что означает что данный файл содержит код на немного другом, улучшенном диалекте С++, имеющем несовместимости с первой (существующей в настоящий момент) версией. При этом оба файла могут сосуществовать в одном проекте, компилируются одним компилятором и линкуются одним линкером. Все нововведения вносить только в версию 2. А лет через 20 отменить версию 1.

ошибка дизайна С

Нет, конечно, это было осознанное и взвешенное решение со своими плюсами и минусами.

Вот в плюсы тащить это было не нужно. Ну т.е. совместимость понятно, но надо было оставлять как есть. Зачем, например, завозили new[] и delete[]?

Тут я бы зашёл с другой стороны. Если new[] и delete[] и так знают размер массива (иначе как бы он тогда удалялся), зачем было делать вид, что он не известен (в случае аллокации). Но это - философский вопрос о совместимости с Си.

new[] и delete[] обязаны вызвать конструктор и деструктор для каждого элемента массива.

На самом деле на целом ряде платформ в прошлом (старые Винды и VС или какой-то Borland) и сейчас (встраиваемые) можно использовать delete после new[] и ничего не ломалось. В винде GlobalAlloc всегда записывает сколько ему памяти попросили выделить, на "салфетке бургомистра", эту информацию можно узнать и вызов освобождения GlobalFree этого значения не требует. А вон на линхе узнать размер выделенного блока, насколько я знаю, нельзя.

А в других случаях нужно передавать размер, но не массива а блока памяти на уровне ОС и он где-то хранится. В других это другая функция. Это зависит от того, как организован менеджер памяти в ОС. И поэтому стандарт разрешает рабочей части delete и delete[] иметь одну и ту же реализацию "рабочей" части, высвобождающей память, но мешать их - это UB.

можно использовать delete после new[] и ничего не ломалось.

Память то оно может и корректно освободит, а что с вызовом деструкторов?

Если известен аллоцированный объем, то и количество объектов, для которых надо вызвать деструктор, известно.

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

стандартный delete не обязан делать, некоторые реализации могут это делать, чтобы иметь общий код для delete и delete[]

Можно и так, по стандарту всё равно UB ) Чисто теоретически интересно посмотреть поведение delete на массиве на разных компиляторах.

Почему ошибка? На мой взгляд, абсолютно логичное решение. Адрес первого элемента хранится в переменной, размер элемента компилятор знает в момент компиляции, больше ему не нужно ничего, чтобы адресовать любой элемент массива. С точки зрения эффективности использования машинных ресурсов решение великолепное.

Непрерывная память - это лучшее, что придумали программисты)

Скорее, речь о правилах пользования этим концептом в нашем языке.

Именно ошибка дизайна языка. Есть стандартный оператор взятия адреса &. Вот его и нужно было использовать для получения адреса массива (т.е. первого элемента массива). А само имя массива должно было стать first-class сущностью, как имя объекта структуры.

Есть стандартный оператор взятия адреса &. Вот его и нужно было использовать для получения адреса массива (т.е. первого элемента массива).

Адрес массива, полученный с помощью &, и адрес первого элемента массива дают совершенно разные типы указателя.

Кстати да:) И это было бы правильно и типобезопасно. Адрес массива был бы адресом объекта, адрес первого элемента - указателем в памяти для перебора элементов. Кажется, в стандарте MISRA требуют явно писать &arr[0] вместо arr.

Это бы, кстати, не отменяло возможность каста такого указателя на такой массив-объект к указателю на его первый элемент, примерно как кастуется указателя на структуру к указателю на первое поле структуры. То бишь явно.

Вообще, мне вот пришла мысль после публикации статьи, что было бы удобно в таком языке "Супер-Си" иметь отдельную штуку под массив элементов известного типа, в котором нет никаких array-to-pointer преобразований, и отдельно - указатель на какую-то "сырую" память. К массивам применять безопасную индексацию, а к указателям на "сырую" память - арифметику указателей. Пускай технически оно реализовывалось бы одинаково, но семантически разница станется огромная. Казалось бы, при чём тут `std::array`...

Это не ошибка, а прямое следствие из принципа «Одинаковый синтаксис у указателей и массивов».

А этот принцип делает удобной прямую работу с памятью (ниша, для которой Си и создавался).

Не имею ничего против прямой работы с памятью, это замечательно и я сам постоянно этим пользуюсь. А вот почему по аналогии с массивами имя объекта структуры не является адресом первого элемента структуры? Вот по этой же причине и имя массива не должно. Для взятия адреса есть оператор &. А имена переменных должны всегда быть объектами. Соответственно, многие чисто синтаксические неоднозначности исчезнут, язык станет логичнее.

А вот почему по аналогии с массивами имя объекта структуры не является адресом первого элемента структуры?

Потому, что индексы — часть работы с массивами и указателями, но не с объектами структуры и самими структурами. Именно индексы — общий признак указателей и массивов, по причине которого им дали общий синтаксис.

Соответственно, многие чисто синтаксические неоднозначности исчезнут, язык станет логичнее.

А заодно исчезнет статический полиморфизм. В виде макросов, которым всё равно, что на входе. В таких формах мне он не нужен (или я не могу вспомнить, для чего нужен), но консистентность убер аллес.

Индексы реализованы через арифметику указателей в С и С++. Но то, что это технически одно и то же не значит, что они должны быть синтаксически одним и тем же. Взять вот такой пример. Можно было бы представить гипотетический язык "Супер-Си", в котором в этом случае происходило бы копирование массива, и sizeof выводил был нормально именно, что размер массива. Не хочется копировать массив - передавай его по указателю. Это была бы нормальная удобная семантика, консистентная со всем остальным в Си.

Но не буду становится еслибыдакабышником)

Никто и не отталкивается от деталей реализации. Всё наоборот: на определённом уровне абстракции массивы и указатели — одно и то же, а значит нужен одинаковый синтаксис.

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

То что имя массива это адрес первого элемента - конечно же ошибка дизайна С/С++.

Имя массива не является адресом его первого элемента.
Ошибка дизайна C касательно массивов — в другом.

*Приводится к указателю на первый элемент в подавляющем большинстве случаев.

А в чём ещё ошибка?

А в чём ещё ошибка?

В том, что это — недотип.

Все нормальные агрегатные типы, вроде struct и union, но кроме массива, позволяют инициализацию переменной значением другой переменной того же типа, откуда сразу следует возможность передачи по значению в функцию и возврата из неё также по значению, потому что передача и возврат по значению — суть инициализация.

Все нормальные агрегатные типы, кроме массивов, позволяют присваивание переменной значения другой переменной такого же типа.

Массив же нельзя инициализировать другим массивом того же типа, и нельзя одному массиву присвоить значение массива другого типа.

При этом существует "особо специальный массив", которым массив проинициализировать всё-таки можно, — строковый литерал.

Проинициализировать можно, а присвоить уже нельзя.

Приводится к указателю на первый элемент в подавляющем большинстве случаев.

Само по себе это не проблема, проблемой является слишком большой набор случаев.

Приводится же функция неявно к указателю на такую функцию?
Приводится же лямбда без захвата неявно к указателю на функцию?

Факт того, что неявное приведение в выражении допускается сколько угодно раз, а также, что разыменование даёт lvalue, позволяет писать:

#include <cstdlib>
#include <iostream>

void fun() {std::cout << "Yes!" << std::endl;}

int main() {
	auto const f{fun};

	(**********f)();
	(**********[]{std::cout << "No!" << std::endl;})();

	return EXIT_SUCCESS;
}

В 7-ой строке функция неявно приводится к указателю на неё и этим значением инициализируется указатель f.

В 9-ой строке указатель на функцию разыменовывается сначала один раз, получается функция.

Затем получившаяся функция неявно приводится к указателю на функцию и разыменовывается ещё раз.

Процесс повторяется 10 раз, но можно сделать, чтобы процесс повторялся значительно большее количество раз.

В 10-ой строке, для лямбды, после её неявного приведения к указателю на функцию, процесс аналогичен.

Набор случаев, когда функция/лямбда приводится к указателю на функцию, сбалансирован.

Например, для лямбд:

#include <cstdlib>
#include <iostream>

typedef decltype([](int const x){std::cout << x << std::endl;}) l_t;

void fun0(l_t const l) {
	l(5);
}

void fun1(void (*const f)(int)) {
	f(7);
}

int main() {
	l_t const l1;
	l_t const l2{l1};
	l_t l3;

	l3 = l2;

	l1(1);
	l2(2);
	l3(3);

	fun0(l1);
	fun1(l1);

	return EXIT_SUCCESS;
}

В 16-ой строке лямбда инициализируется значением другой лямбды.
В 19-ой строке одна лямбда присваивается другой.

В 25-ой строке лямбда передаётся в функцию fun0 по значению.
В 26-ой строке лямбда неявно приводится к указателю на функцию и потом передаётся в функцию fun1.

Нет вот этого "недотипства", как у массива, и неявное приведение не мешает, а когда надо — работает.

Хотя, конечно, для лямбд без захвата эти действия смысла особого не имеют, но недотипом в смысле массивов лямбды не назвать.

При этом, если массив "обернуть" в struct или union, то это обёрнутое начинает прекрасно использоваться для инициализации переменной того же типа, а посему и прекрасно передаваться и возвращаться из функции по значению, а также прекрасно присваиваться одно другому.

Но сделанного не воротишь, массив — недотип.
Хоть он и недотип, но — никакой он не указатель.

В очередной раз спасибо за подробный комментарий! Особенно нечего и добавить.

Кстати, в приколе с приведением функции к указателю в вашем примере забавно то, что и в IR, и в ассемблере ничего этого нет даже без оптимизаций. В какой-то мере, операция это максимально бессмысленная, но всё-равно существующая.

НО!

В случае с лямбдами всё иначе. Я дополнил ваш пример обычным вызовом лямды. В случае, когда к лямбде сначала применяется операторы разыменования, в ассемблер они всё-таки просачиваются (ну, одна операция). Код для оператора приведения к указателю на функцию (синтаксис, кстати, бодрящий!) генерируется. В отличии от случая, когда происходит просто вызов лямбы - там есть только один вызов call непосредственно сгенерированной функции.

А может просто не нагружать обычные указатели лишними смыслами и свойствами. Указатель - это всего лишь адрес начала объекта в памяти, а наличие у него типа предотвращает часть ошибок на этапе компиляции. Вот в принципе и всё. Это просто как косвенная адресации на ассемблере. Если хочется совсем уж умных указателей, то можно и свой тип навернуть.

Да. Но речь про массивы.

Но и про указатели на массивы и сопутствующие проблемы...

Да. Но в контексте массивов не программист наделяет указатели дополнительными смыслами, а стандарт С++ (и Си). С этой точки зрения как раз и можно сказать, что тот же std::array позволяет сделать то, о чём вы и говорите.

Я не понял в чем была проблема. Даже в последнем примере, где

auto ptr = reinterpret_cast<std::remove_all_extents_t<decltype(arr)>*>(arr);

применяется забористое заклинание вместо

int* ptr = &arr[0][0][0];

И все, дальше гоняйте по ptr[i] от нуля до sizeof(arr) / sizeof(int). Можете даже поставить указатель на доступную область памяти и прочитать блок хоть массивом, хоть одной переменной через адресную арифметику.

Колдунство там для случаев, когда не известно, сколько скобочек ставить. Например, если бы я хотел написать свой flat(). Но это было бы UB.

UB есть и в вашем примере, строго говоря, по тем же причинам.

А "свой" flat написать нельзя, так как Си++ предполагает очень обобщенную архитектуру. Я не знаю ни одного нативного компилятора на котором это бы не работало, но есть же и компиляторы Си++ в языки виртуальных машин.

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

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

Нигде не сказано что область памяти массива кончается там же, где конец области памяти его последнего элемента. Это де-факто так на железных реализациях. Там может быть что-то еще, например, та же информация выделенном блоке памяти. Ну и компилятору (или ВМ) будет сложно проследить корректность обращения к памяти, если границы массива где--то хранятся и проверяются во время исполнения.

Ну вот и да. УБ бояться, под МК не писать. Абстрактность указателей кореллирует с абстрактностью подхода.

Конечно, если бы idSoftware боялись UB, они бы не написали быстрый обратный квадратный корень. Но UB на то и UB, чтобы о работе программы нельзя было делать предположений после его возникновения.

Ох уж это УБ... УБ - это не правила языка, УБ - это ограничения оптимизирующего компилятора, который с лёгкостью даст протечь потоку выполнения функции в недопустимую область. Так что я даже больше скажу. Делать предположения о работе программы с УБ нельзя уже после компиляции. УБ не возникает, это не событие, это сломанный код. УБшность можно регулировать опциями компилятора.

Не совсем понял вашу мысль. UB вполне себе фигурирует и в Сишном, и в плюсовом стандартах. Так что это волне себе правило языка. "Вот ситуация, вот там нам меня не расчитывай", фактически. По хорошему, UB - это ограничения на способы написания кода, которые делают этот код более портабельным.

Вот и я к этому же и веду, что УБ - именно, что правила написания переносимого кода, стандарт компиляции. То есть, под разные платформы список УБ должен быть разным в том числе и по архитектурным причинам. Про агрессивную УБ оптимизацию и так понятно.

Если посмотреть с этой стороны, то если вы пишите под конкретный компилятор на конкретной платформе - то и UB бояться нечего, так как вы знаете, что компилятор сделает.

А так единый список UB же и делает код переносимым, разве нет?

Да, с единым списком можно писать переносимый код. Теперь бы ещё в среде разработки иметь подсветку УБ.

Но мне сам подход разработчиков компиляторов вызывает негодование. Встретил УБ, выкинул кусок кода, функция потекла, да и хер с ней, УБ же! Нет чтобы встретил УБ, откомпилировал кусок кода без оптимизации, 1 в 1, как это предполагает язык для выбранной архитектуры, и молодец. Можно же, но нет.

УПД. Вот мы тут обсуждаем примеры с УБ, которые синтаксически верные, они зачастую даже корректно компилируются, мы все однозначно понимаем, что автор примеров пытается достичь в них, но там УБ, а значит потенциальный апокалипсис. Ну бред же!

Ещё УБ можно не компилировать, а ругаться, что вот мол у тебя здесь выход за пределы массива или разыменование nullptr.

Но нет, зачем, по стандарту можно же диск отформатировать.

Не совсем. То, о чём вы говорите, это implementation-defined behaviour. А вот UB, к сожалению - это формально действительно что угодно и даже в двух последовательных стейтментах или запусках компилятора может быть скомпилировано по-разному

И вот как производители компиляторов это допустили? Оптимизация, основанная на предположениях и допущениях снижает надёжность результата компиляции.

Ну...она не снижает надёжность результата компиляции программы, если в программе нет UB) Но тут мы начнём ходить по кругу уже)

Я как понимаю, у UB есть два глобальных прикола:

1) Неопределённое поведение не может быть определено, так как, это может привести к замедлениям на каких-то платформах. А в С++ мы не платим за то, что не используем.

2) Компилятор исходит из того, что UB в программе нет, даже когда оно там есть.

То есть, на уровне философии самого языка предполагается, что следить за UB должен не компилятор, а программист.

Сделать язык максимально переносимый лишь путём запретов части кодовых решений, по моему мнению, является ущербной стратегией стандартизации языка. То есть сейчас, я, по мнению комитета, не вправе использовать особенности целевой платформы и ожидать определённого поведения. Но как эмбеддер именно для этого я и пользуюсь Си.

Согласен, пусть компилятор исходит из того, что в программе нет UB, но он не вправе выкидывать кусками код не отслеживая целостность. Даже мёртвый, по мнению компилятора, код нельзя выкидывать. Т.к. на него может быть совершен неявный прыжок ради целей недоступных пониманию компилятора. Надо исходить из того, что всё что пишет программист, он пишет с целью выполнить.

Так что, если стандарт против здравого смысла архитектуры, тем хуже для стандарта.

Хотя чего это я, у компилятора -O0 же есть!

Надёжность результата компиляции обеспечивает стандарт. Не совсем понимаю, почему этого недостаточно.

Очень странный подход что к массивам, что к указателям. Честно - если чётко понимать что есть что - никаких проблем не возникает, возможно размеры моих проектов недостаточны для того чтобы почувствовать все это, но единственное что у меня было это выход за границу.

Главная проблема - это понять что такое указатель в C, причём реально понять, а не проверхостно - это убирает большинство проблем и ошибок.

А в целом что-то смущает или хочется ограничений - сделай свой класс или воспользуйся либой, не хочешь - используй стандартные примитивы - в этом вся сила C.

Конечно, про понимание вы написали абсолютно правильно. Кстати, про понимание указателей есть отличная статья.

Главная проблема - это понять что такое указатель в C, причём реально
понять, а не проверхостно - это убирает большинство проблем и ошибок.

Нетрудно понять, что такое указатели, трудно удерживать в голове всю сеть указателей, указателей на указатели, указателей на указатели указателей...

Если дело дошло до указателей на указатели на указатели, то это дело пахнет керосином.

Не, ну там же не `int ***********ptr`) Я не вдавался в подробности эффективной имплементации списков, но там, всё-таки, должна быть структура, в которой данные и указатель на структуру. А указателя на указатель ... на указатель там, вроде, нет.

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

Ну, сконвертировать указатель на структуру в указатель на первое поле структуры можно. Так что в этом случае вы правы)

чистого *** синтаксиса нет

Видишь суслика? И я не вижу, а он есть :) Вот только на днях натыкался:

extern SANE_Status sane_get_devices (const SANE_Device *** device_list,
 SANE_Bool local_only);

Фактически - указатель на указатель на массив указателей на пачку структур.

Вот только на днях натыкался

Ещё пример - в POSIX есть scandir(в dirent.h):

       int scandir(const char *restrict dirp,
                   struct dirent ***restrict namelist,
                   int (*filter)(const struct dirent *),
                   int (*compar)(const struct dirent **,
                                 const struct dirent **));

Ну кстати да. Тут тоже выходит указатель на массив указателей. Спасибо!

Список указателей на указатели?

А еще есть антипаттерн:

typedef void *SANE_Handle;

Без заглядывания в исходник попробуй сходу догадайся, что тут заныкан указатель, а не просто алиас или структура/перечисление.

Как понимаю, sane - это API. Пользователю, по хорошему, не нужно думать, что во что тайпдефнуто, пока имплементация сама со всем хорошо разбирается. Но void * - штука опасная, если он кастится куда-то не туда, это точно.

Очередная хайповая тема. Сточки зрения машинного кода массивы прекрасны (всё быстро и компактно). Если важна максимальная производительность какого то участка кода - это идеальный вариант. Если вам важно удобство, защита от выстрела в ногу, если в проекте несколько программистов и хочется изолировать интерфейс от реализации - не используйте массивы таким способом, оберните в класс, который будет помнить размеры всех размерностей и не допускать выход из диапазона при доступе к элементам. Всё ж просто, спорить не о чем.

Под этим комментом пошло обсуждение скорости, думаю, лучше продолжить там.

Но говоря об обёртках - зачем делать свою обёртку над массивом, если есть std::array?

UFO just landed and posted this here

Я звиняюсь, а какие аналоги есть у С++?

Go слишком мал для такого. У него мало возможностей, за которые многие(и я тоже) любят C++. Вместо того, чтобы добавить возможностей в язык и сделать его чуть сложнее, они выбрали путь максимального упрощения, из-за чего много бойлерплейта, писать код неприятно. Я считаю такой подход в корне неправильным.

Go по сравнению с С можно простить всё что угодно только за штатный способ возврата ошибки (т.е. result, error = f(x) )

В современном многопоточном асинхронном мире сишный errno это даже не костыль, а якорь, не дающий всплыть.

Так а мы то сравниваем с C++, а не с C.

А какой штатный способ возвращения ошибки в плюсах? Исключения труЪ посоны вырубают, да и спецификацию исключений из плюсов давно выпилили. Тип результата/ошибки здорового человека ( std::expected<T,E> ) завезли, .., только в С++23. Когда это уже никому не нужно, потому что все основные библиотеки, включая стандартную, написаны без него. Да и то, случилось это только из-за давления массы людей, уже вкусивших раста и голанга.

Можете пояснить, где выпилили спецификацию исключений? Потому что в стандарте всё ещё есть соответсвующий раздел.

Штатного способа в языке нет (и не предполагается). Программист сам решает. std::expected естественно можно реализовать и смому, или заиспользовать библиотеку. Можно воспользоваться и std::optional, но тут конечно спорно.

Ещё раз по поводу исключений. C++ без исключений не C++. Их невозможно выключить (даже через компилятор, так как невозможно гарантировать, что линкуемая библиотека их не бросает). Исключения ускоряют код и весьма полезны, несмотря на то, что портят поток управления. Не могу сходу вспомнить письменного примера, по этому предлагают послушать доклад.

Имеется ввиду очень старая спецификация исключений вида (пишу по памяти)

int f() throws std::bad_alloc, std::bad_cast;

Она давным-давно выпилена, т.к. несовместима с шаблонами (подшаблонный тип может швырять что угодно). Её аналог сохранился в Java. В плюсы позднее завезли частный случай с noexcept, но это для экономии на спичках.

Вообще говоря, это очень плохо, потому что фактически неизвестен полный тип функции, а именно какие исключения она может вышвырнуть.

Вообще-то спецификация исключений из C++98 не проверялась в compile-time (это не был аналог checked exceptions из Java). Проверка делалась в run-time и вызывался std::terminate если вылетало исключение другого типа. Но сам компилятор по рукам за попытку бросить исключение из функции с декларацией throw не бил вообще.

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

В плюсы позднее завезли частный случай с noexcept, но это для экономии на спичках.

Это как бы вообще не аналог. Ну вот совсем. И назначение у noexcept другое, а именно -- показать насколько безопасно вызывать функцию/метод в специфических контекстах (например, в деструкторах, секциях catch, в других noexcept-функциях, вроде swap или move operator).

Вообще говоря, это очень плохо, потому что фактически неизвестен полный тип функции, а именно какие исключения она может вышвырнуть.

Если проект нормальный, то все исключения там (прямо или косвенно) наследуются от std::exception. Так что это не такая большая проблема, как об этом говорят.

Хорошее замечание! "Exception specification" проверяется в конце personality routine (обычно), ровно так же, как и сам брошенный эксепшн, то есть через type_info.

А noexcept также оптимизирует код, так как убирает lsda из функции, вызывающей noexcept функцию. Вот тут дали ссылку на обалденный доклад, в котором говорится, что это прирост к перформансу не слишком большой, правда.

Возможно, вам понравятся "контракты", которые стараются втащить в стандарт. Они синергируют с исключениями и составляют часть сигнатуры функции.

Я тоже люблю всякие языковые фичи, это интересная тема сама по себе. Но вот на Go написал недавно свой проект (выкачивание и анализ данных из соцсети) без ООП, интерфейсов и дженериков, и с минимальным использованием многопоточности и каналов. Просто функции и структуры в стиле Си. И всё получилось просто и понятно.

Go не аналог плюсов. Rust - возможно, но меня напрягает его централизованная экосистема. Я таким уже не доверяю, какие бы благие цели они ни декларировали.

Ну и чего-то аналогичного Qt у Раста пока нет. Вообще с GUI как-то не густо у него, я из графических программ на Rust встречал только amdgpu_top с простейшим GUI.

А если чисто эстетически, то синтаксис у Раста просто ужас даже на фоне плюсов :)

Всё же, стоит признать, в некоторых местах синтаксис раста всё же лучше. Те же лямбды, или что можно не писать return в некоторых случаях. Как минимум с точки зрения того, что он просто короче. А всё же возвраты из функция и лямбды, это очень часто применяемый инструмент. А вот всё остальное чисто дело вкуса, мне тоже больше синтаксис плюсов нравится(Наверное потому что я на них пишу).

UFO just landed and posted this here
Иф ю ноу, ю ноу

Вы решаете за меня, что мне нужно? Спасибо, но нет.

В заголовке стоило написать "C" - а то был уверен, что речь про std::array.

Про std::array там речь тоже идёт.

Изначальная идея текста была именно в том, чтобы рассмотреть build-in массив именно в рамках плюсов. И в практической, и в теоретической части рассматриваются случаи, которых нет в Си.

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

Про std::array там речь тоже идёт

Но '_____' относилось именно к C массивам - а я зашёл посмотреть, что же не так с std::array.

Если '_____' - которое в статье, то это задумывалось, как отсылка к Большому Кушу)

А с std::array всё так)

Не мог ли бы Вы указать источник цитаты из КДПВ?

Могу сослаться только на голову автора, сиречь меня. Заглавка - адаптация мема с Кодзимой.

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

Но вот от сишного кода никуда не деться и каждое взаимодействие с ним очень муторное. Забыл где-то одну проверку и у тебя сегфолт.

Modern C++ мы получили, осталось только дождаться С++2)

А так да, если можно использовать более надёжные конструкции без проблем в перформансе проекта - это же здорово!

Слушайте, а где почитать, почему gcc выдаёт такой стрёмный код для 3-мерного массива?

Там же вообще нет нормального ret

Код выглядит так из-за санитайзера. Компилятор с включенной опцией -fsanitize=undefined добавляет в исходный код нужные ему проверки. По теме есть лекция от разработчика asan https://www.lektorium.tv/lecture/23702

Переформулирую вопрос - почему в принципе этот код падает? Ну т.е. что конкретно даёт компилятору творить такую дичь?

Потому что обращение к past-the-end, полагаю, и, как следствие, UB, которое и позволяет. Если есть arr[i][j][k], то выражение arr[i][j] есть некий массив sub_arr[k], и если за k выходим, то получаем UB.

Нет, в случае многомерных массивов выход за пределы внутреннего массива UB не является.

Есть куча либ, например, для обработки изображений, где этот паттерн очень активно используется. Когда нужно, ходят по [y][x], а в других случаях по [0][i]

а можно цитату про UB? а то я что-то в упор не вижу, поиск тоже не помогает.

An array subscript is out of range, even if an object is apparently accessible with the given subscript (as in the lvalue expression a[1][7] given the declaration int a[4][5]) (6.5.6).

Язык Си явно свернул куда-то не туда. Я бы ещё понял Unspecified Behaviour - например, на ряде платформ подобный трюк потенциально может вызвать аппаратный Alignment Fault. Но язык С тем и был ценен, что можно было сказать компилятору "заткнись, я знаю что делаю". Но вообще убирать ret, т.е. ветку штатного выхода из процедуры, как в примере выше, - это уж какой-то совсем лютейший перебор.

Не понимаю, как на этом ещё можно что-то писать в 2024. Софт не должен настолько ненавидеть кожаных мешков.

Ну. Сложно спекулировать о том, какие приколдесы могут случиться при UB, пока такие прикалдесы не встретились на практике. Например, в этом восхитительном выступлении на CppCon есть описание совершенно зубодробительного бага, следующего как раз из UB.

Если вернуться к нашему примеру, можно попробовать представить ситуацию в которой, например, двумерный массив аллоцирован на двух страницах памяти. В конце первой страницы аллоцирован первый под-массив и кусочек второго. В начале второй страницы аллоцирован остаток второго под-массива. И вот я начинаю обходить весь двумерный массив целиком через первый под-массив. Если в каждом моменте в коде, где идёт такая итерация, используется именно обход через первый-подмассив, то может ли вторая страница быть возвращена обратно ОС? Семантически, никаких манипуляций со вторым под-массивом не происходит. А что произойдёт, если такая страница будет возвращена ОС, а потом из неё в реальности произойдёт чтение? Я такое не встречал, но вроде как ничего не мешает подобной штуке случиться.

может ли вторая страница быть возвращена обратно ОС

нет, не может, потому что выделялся массив целиком (обе половины единым куском). Это тоже гарантия.

Ещё раз повторю, может быть AlignmentFault. Если допустим в самом внутреннем массиве нечётное количество элементов, и используется SIMD

C++20 § 9.3.3.4/9 говорит:

[Note: When several “array of” specifications are adjacent, a multidimensional array type is created; only the
first of the constant expressions that specify the bounds of the arrays may be omitted. [Example:
int x3d[3][5][7];
declares an array of three elements, each of which is an array of five elements, each of which is an array of
seven integers. The overall array can be viewed as a three-dimensional array of integers, with rank 3 × 5 × 7.
Any of the expressions x3d, x3d[i], x3d[i][j], x3d[i][j][k] can reasonably appear in an expression. The
expression x3d[i] is equivalent to *(x3d + i); in that expression, x3d is subject to the array-to-pointer
conversion (7.3.2) and is first converted to a pointer to a 2-dimensional array with rank 5 × 7 that points to
the first element of x3d. Then i is added, which on typical implementations involves multiplying i by the
length of the object to which the pointer points, which is sizeof(int)×5 × 7. The result of the addition
and indirection is an lvalue denoting the ith array element of x3d (an array of five arrays of seven integers).
If there is another subscript, the same argument applies again, so x3d[i][j] is an lvalue denoting the jth array element of the ith array element of x3d (an array of seven integers), and x3d[i][j][k] is an lvalue
denoting the kth array element of the jth array element of the ith array element of x3d (an integer). — end
example] The first subscript in the declaration helps determine the amount of storage consumed by an array
but plays no other part in subscript calculations. — end note]

Выделил интересное.

C++20 § 7.6.6/4 говорит:

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.
—(4.1) If P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value.
—(4.2) Otherwise, if P points to an array element i of an array object x with n elements (9.3.3.4),77 the
expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) array
element i + j of x if 0 ≤ i + j ≤ n and the expression P - J points to the (possibly-hypothetical) array
element i − j of x if 0 ≤ i − j ≤ n.
—(4.3) Otherwise, the behavior is undefined.

Выделил интересное.

Насколько я это понимаю, одназначное UB. Можете предложить интерпретацию без него?

Проверил этот всратый пример. Если заменить в условии цикла 8 на 2, то всё компилируется нормально.

Другими словами, если разнести инициализацию массива и его использование по разным функциям, то всё будет работать, никаких UB не возникает.

Говорите мне что хотите, но это 100% баг компилятора gcc

Если в примере заменить условие цикла 8 на 2, то не будет UB в первую очередь.

Что не так с std::array? Есть тип, размер. Что еще нужно то?

С std::array всё так. Можно сказать, что статья про то, что с ним-то всё как раз в порядке.

Массивы в С/С++ это указатель и все. Количество данных по указателю и их размер хранятся во вне с помощью дополнительных усилий программиста.

Массивы в С++ - это не просто указатель. Как минимум, это указатель, к которому можно применять оператор сложения с числом. В противном случае получится UB.

Плюс, размер даже build-in массива известен компилятору (то есть без усилий программиста) до того, как произошёл array-to-pointer.

А разве к обычному указателю нельзя применять сложение с числом?

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

Т.е. это:

#include <iostream>

int main()
{
    unsigned long a = 0xFFFEFDFC;
    unsigned char *p = (unsigned char *)&a;
    p += 2;

    printf( "%x", (unsigned char)*p );
    
    return 0;
}

UB? Или я что-то не так понимаю?

Формально - да, так как lifetime массива не начался. Тут, вроде как, идёт дискуссия о внесении в том числе такого в стандарт. Это ещё к вопросу о том, что возвращает `malloc` - указатель на элемент массива, или нет?

Другое дело что это везде работает. Но если хочется прямо формально безупречно, то можно сделать что-то такое.

malloc - указатель на элемент памяти имхо, причём тут массив?

А то, что выше я писал - в куче библиотек используется, очень сомневаюсь что стандарт, ломающий все это, примут.

Ну и в страндарте есть странное possibly hypotetical array - так вот, любой кусок памяти это possibly hypotetical array of bytes.

Там не "possibly hypotetical array", а "possibly hypotetical array element". Это костыль (имхо) для того, чтобы указатель мог указывать (sic) на элемент, следующий за последним в массиве, чтобы можно было писать `start < end' в цикле.

Про malloc я имел в виду, что указатель, из него выплёвываемый, формально нельзя использовать для итерации, так как под ним формально нет массива.

А так ясен-красен, что работает, как бы ещё тот же memset был сделан.

Интринсиками, как же ещё? Функции стандартной библиотеки вполне могут работать не так, как весь остальной язык и делать то, что просто так пользователю языке не реализовать. Примеры: std::launder, std::bit_cast

Стоп. Просто к указателю тоже можно применять сложение с числом. Вроде массив отличается только наличием размера (что усугубляется в случае многомерных массивов). Лучше б вообще не отличался, меньше граблей бы было.

Формально не можно. В соседнем трэде описано, почему.

Что значит "не можно"? Выделил массив, взял указатель на любой элемент и алга!

Ну, если вы делаете new[], то стандарт вам гарантирует наличие массива. Если вы делаете malloc, то такой гарантии у вас нет. В комментарии указали на целый пропоузал в стандарт, который в том числе с этим хочет разобраться.

Опять же, мы говорим конкретно сейчас про то, что написано в стандарте.

Ну да, но это всё касается не указателя как такового, а откуда вы можете его получить. Указатель после malloc – это вообще указатель просто на память, так что у вас будет C-style cast или ещё что – там и опасность, не в операциях с указателем.

как говорит ТС, они не просто указатель, но всех описанных проблем можно навсегда избежать если относиться как ним, как к указателям и делать как вы говорите: хранятся во вне с помощью дополнительных усилий программиста, или использовать контейнеры. Я так и делаю )

5 минут квалифицированного ворчания))

А как надо понимать, допустим, l = (p+2) из исходного примера соответствующего 3-му элементу массива? И что должно возвращать sizeof от этого l? Или p это указатель на встроенный массив, а l - на его часть с 3 элемента?) То есть не просто int указатель?) А если всё таки просто, то значит между p и l тоже произошло какое то сужение исходного представления arr? А почему тогда не смириться, что оно происходит сразу при p=arr?

Разобраны хорошие примеры, и да, массивы источники минимум беспокойства, и альтернативы не зря появились, но указатели и преобразование при присвоении тут не очень причём - просто массивы такие, и с ними приходится так жить, если нужно)

Извините, я не очень понял, что вы имеете в виду) Если спросите другими словами, то отвечу.

За похвалу спасибо)

Я не увидел предложений по альтернативному поведению и лишь попытался порассуждать, каким бы оно могло быть - и не нашёл ответа) Поэтому живём дальше.

А, ну... Я вот тут попытался немного порассуждать о языке "Супер-Си")

В Фортране, ну где то точно, массив это дескриптор из С массива и длины. И ОС, когда из кернела копирут данные, не в курсе про длину какого то инстанса в юзер спейс. И очень часто содержимое этого массива не интересует логику, которая его обрабатывает. Я всё к тому, что массив это массив, это реальность, оборачивание может случиться на другом уровне абстракции, если надо

https://gcc.godbolt.org/z/1bh4d616d

Что-то с лямбдами неправда написана. array-to-pointer conversion выполняется только в случае [arr=arr]

#include <iostream>

int main() {
    int a[] = {1,2,3,4,5,6,7,8,9,10};

    [=]() { std::cout << sizeof(a) << std::endl; } ();    // 40
    [&]() { std::cout << sizeof(a) << std::endl; } ();    // 40
    [a]() { std::cout << sizeof(a) << std::endl; } ();    // 40
    [a=a]() { std::cout << sizeof(a) << std::endl; } ();  //  8
}

Там так и написано.

Поднимите руки те, кто никогда не использовал sizeof для C-шных массивов.

В ту же степь – многомерные массивы. Я изучал C после Паскаля, и изначально привык считать, что их нет вообще, и если тебе нужен многомерный массив (обычно нужны неизвестного заранее размера) – выделяй одномерный и играй с индексами.

К слову, никто не мешает использовать одномерный массив как многомерный при условии, что вся математика происходит внутри квадратных скобок)

Именно!

А поскольку обычно массив итерируешь по строкам/столбцам – один раз вычисляешь приращения индекса dx/dy и делаешь i+=dy для индекса в массиве или p+=dy для указателя.

2d матрицы молотите?)

Лет 25 назад по работе молотил (численное моделирование теплового взрыва). Ну и позже, когда анализом изображений занимался.

В статье упущен рабочий вариант с передачей сырых массивов по ссылке с размером в шаблоне:

template<size_t N>
void func(int (&arr)[N])
{
  ...
}
...
func({1,2,3});

Только const int (&arr)[N]. Вы передаёте prvalue.

А так да. Опускал сознательно. Про ссылки на массив вкратце упомянул в теоретической части.

Sign up to leave a comment.