Как стать автором
Обновить

Комментарии 58

Читая эту статью сразу захотелось оставить комментарий, но не могу выбрать какой:
  1. Плюсы — это когда получение длины массива — тема для статьи
  2. Плюсы — это когда есть 3 способа создать массив, из которых 2 неправильных

P.S. Естественно, все это шутки, но пройти мимо не смог
Эх, все три неправильные. По-хорошему надо было бы чтобы auto myArr = [1, 2, 3]; создавало объект типа std::array и у него там уже кому надо мог бы брать размер. Но седые мудрецы в комитете плюсов расскажут вам 100500 причин почему такой простоты допустить нельзя никогда.
Для этого есть Python, ненужно все яйца складывать в одну корзину.
так в статье написано, такая простота уже работает сейчас.
std::array myArray = {2, 7, 1, 8, 2};

я так в коде и использую.
Мудрецы? Проблемы?)
есть еще
auto myArray = {2, 7, 1, 8, 2};

который создаете initializer_list у которого тоже можно брать размер. То что init_list — дерьмо, другой разговор, речь про то что «вот просто взять простым синтаксисом объявить » ;)

Почему же нельзя?
std::array arr = {1,2,3}

Это дичь по целому ряду причин. Вот давайте посмотрим на питоновский array:
arr = [1, 2, 3]

И это уже объект массива, у него всякие методы можно вызывать. Теперь посмотрим на
std::array arr = {1,2,3}

Вот как мы объясним новичку, что тут происходит? Начать надо с std::. А для этого надо рассказать об областях видимости, неймспейсах и стандартной библиотеке. Рассказать, что лежит в std. Ладно, дальше array. А почему инициализируется списком в фигурных скобках {} а не как везде (и даже в С++ для «голых» массивов!) — квадратными? Надо рассказывать о списках инициализации. Потом объяснять, почему не скомпилилось, ведь надо ещё правильный include написать.

И ведь на всё есть свои причины! Из-за которых новичек посмотрит на это дело, плюнет, и уйдёт.
3. Плюсы — это когда антиязык

Почему для size везле int, а не size_t или auto?
2020 год же уже.

Страуструп и компания на одной из QnA сессий шесть лет назад ответили на этот вопрос. Где-то с 12:55 (сам вопрос на 9:50) и 43:14
https://youtu.be/Puio5dly9N8

НЛО прилетело и опубликовало эту надпись здесь
Из зала: — Вот вы говорите, в общем случае нужно использовать знаковые числительные, но в STL они все почему-то беззнаковые.
Саттер: — Это неправильно.
Каррут: — Извините.

Чуть более развёрнутый ответ от Страуструпа:
— Это приводит к чрезмерно большому количеству ошибок.
НЛО прилетело и опубликовало эту надпись здесь
Достаточно часто приходится использовать относительные индексы, вроде a[n-2].
Это приводит к необходимости преобразования типов, причём сплошь и рядом.
При этом при использовании беззнаковых типов индекс (unsigned)(-1) куда опаснее чем знаковый -1 потому что проверить его валидность сложнее.
Однако непонятно, почему. Unsigned позволяет как минимум не проверять переданный в публичном АПИ индекс на < 0.

Проверять на <0 бессмысленно, проверка всегда возвращает false.
А ошибки могут быть следующие:
1)


for(unsigned int i = 100500; i>=0; i--){...}

2)


unsigned int a = 3;
unsigned int b = 4;
unsigned int i = a - b;
if(i < 0) return; 
... use arr[i] ...

Это даже если signed и unsigned не смешивать.

НЛО прилетело и опубликовало эту надпись здесь
Если публичное API signed

Речь шла о unsigned. Будет тот же sigsegv (ибо в дополнительном коде a+((unsigned int)-1) == a-1, но проверкой <0 такой "-1" не обнаружить, он равен например 0xffffffff.


Во всех случаях — ошибка программиста программы, а не программиста API.

Ошибки — они и есть ошибки. Их надо исправлять, а не перекладывать.
P.S. Кто такой "программист программы" и чем он отличается от обычного программиста?

НЛО прилетело и опубликовало эту надпись здесь
Касательно проблем с «нельзя отвалидировать индекс» — а разве то, что другой программист прислал в ваше публичное АПИ некорректный индекс — это проблема создателя публичного АПИ? Это проблема ТОЛЬКО того программиста, который написал некорректный код

В некотором роде, это проблема создателей языка, когда i>=0 то работает, то не работает в зависимости от типа i. Та же java намеренно не имеет числовых беззнаковых типов, чтобы быть надёжнее проще для не всегда внимательных кодеров.


Проверив индекс на < 0 если он сделал какую-то математику вы только в лучшем случае заглушите ошибку.

Ну в реальном коде я бы там исключение бросал, чтобы наверняка, это лишь пример с return. SIGSEGV-то не при каждом доступе за пределами массива возникает, так что это ненадёжная проверка, что ошибок нет. Тут вопрос в том, что проверка в моём примере вообще нерабочая.

В некотором роде, это проблема создателей языка, когда i>=0 то работает, то не работает в зависимости от типа
беззнаковость — это дополнительная информация, которую вы и тулинг можете использовать. Ваше «i>=0 не работает» — очень странная претензия. Вам банально компилятор или любой анализатор выдаст предупреждение об избыточности условия. Вы ведь не обязательно хотели итерироваться до -1, возможно, вы хотели написать «i > 0» или «i != 0», или что-нибудь еще в таком духе.
Вы ведь не обязательно хотели итерироваться до -1, возможно, вы хотели написать «i > 0» или «i != 0»,

Нет, я хотел итерироваться до 0 включительно (не до 1 и не до -1), поэтому написал i>=0. При чем тут i>0 и i!=0? В этом случае описанной проблемы не было бы. Проблема в i>=0.


Да, к слову, проблемы возникают и на граничных значениях signed типов (abs(INT_MIN) не влезает в int), и на пределах точности float-типов. Но у unsigned граница 0, и это очень близко по величине к тем значениям, которые обычно используют, поэтому проблемы тоже находятся ближе.

Нет, я хотел итерироваться до 0 включительно (не до 1 и не до -1)
в случае signed счетчика ваш цикл остановится на значении -1.
При чем тут i>0 и i!=0? В этом случае описанной проблемы не было бы. Проблема в i>=0.
проблема в вас — вы хотите чтобы программа вела себя не так как вы её написали.
Да, к слову, проблемы возникают и на граничных значениях signed типов (abs(INT_MIN) не влезает в int)
это особенность двоичного представления целых чисел в общем, и вообще никак не относится к беззнаковым
Но у unsigned граница 0, и это очень близко по величине к тем значениям, которые обычно используют, поэтому проблемы тоже находятся ближе.
какая разница, пытаетесь вы обратиться к -1-му или 9'223'372'036'854'775'807-му элементу массива, если и то и другое — UB?
Ещё вот тут Вандевурд про вред беззнаковых типов в циклах пишет.
Вообще, этот комментарий 12 сентября в ~19:30 был написан.
4 месяца на премодерацию ушло ))
Что до подсчета, то компьютеры в этом чертовски хороши. Так пусть же считает компьютер!

А если нужно задать массив из 100 элементов, где первые {1,2,3}, а остальные нули?
А если вообще нужен неинициализованный массив?

По-моему вредность этого «антипаттерна» в представлении автора (заметил, что перевод) сильно преувеличена.

Это должно быть переписано по крайней мере как:
std::array<int, 5> myArray = {2, 7, 1, 8, 2};

Ну то есть когда пишем просто int myArray[5] — «магическое число» это плохо, а вот когда std::array<int, 5> — это уже совсем другое дело…
А если нужно задать массив из 100 элементов, где первые {1,2,3}, а остальные нули?
А если вообще нужен неинициализованный массив?
По-моему вредность этого «антипаттерна» в представлении автора (заметил, что перевод) сильно преувеличена.

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

А если нужно задать массив из 100 элементов, где первые {1,2,3}, а остальные нули?

Иногда чтение кода на c++ напоминает мне чтение какого нибудь Толстого, Достоевского, Чехова… Всегда при втором прочтении обнаруживаются скрытые смыслы и акценты. А что имел ввиду автор? Умышленно ли он заставил говорить своего героя эту фразу? Или это ничего не значащий эпизод из трехтомника?
Ну то есть когда пишем просто int myArray[5] — «магическое число» это плохо, а вот когда std::array<int, 5> — это уже совсем другое дело…
во втором случае у вас всегда будет arr.size(), который вернет то самое волшебное число. Даже при попытке передать этот массив куда-то.
int myArray[] = {2, 7, 1, 8, 2};
constexpr int myArraySize = std::size(myArray);

Это требует C++17. Вариант ниже сработает на любой кофеварке и в обычном C:

int myArray[] = {2, 7, 1, 8, 2};
int myArraySize = sizeof(myArray)/sizeof(myArray[0]);

А потом перенесли myArray в кучу и привет.

И-и-и… Что?

int *myArray = (int *)malloc(5 * sizeof(int));
int notMyArraySize = sizeof(myArray)/sizeof(myArray[0]);

Тогда непонятно, зачем размер массива второй раз вычислять, если он заранее, на этапе malloc(), известен.

Конечно когда я показал вам две строчки то всё понятно. Но такие ошибки встречаются, когда что-то поменяли, но забыли сделать во всех местах. Особенно у новичков, но и опытные не застрахованы от глупых ошибок. Обычно компилятор должен помогать в таких случаях, потому что по факту тип переменной поменялся. В С++ он действительно меняется с std::array на std::vector, например. А в Си всё есть указатель причем в худшем случае ещё и на void*, т.е. следить надо самому.

какой смысл с++ программисту писать код так, если есть вектор или std::unique_ptr для массива или std::unique_ptr для std::array?

Тогда получится sizeof(std::vector), или какое ваше предложение?

ну так хорошие c++ программисты и не будут вычислять размер массива/вектора/чего угодно через sizeof, а будут использовать либо std::size, либо T::size

Перечитайте ветку, об этом и речь.

Иногда приходится поддерживать чужой код — в том числе написанный в С-стиле с malloc и прочим ужасом…

Ещё есть std::experimental::make_array :)

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

выделяю буфер с запасом небольшим
Этот бред уже вошёл в анналы истории:
gamedev.ru/flame/forum/?id=122958
Что делаешь в случае, если новое сообщение не помещается в буфер?

Делаю так, чтобы помещалось ;-)
Свежий пример: логирование цепочки датчиков температуры в порт. Буфер, условно, 100 байт. Один буфер — одна строчка лога. Сначала "шапка" (время и т.п.), потом, строчка за строчкой, показания датчиков. Заполнили, отправили. Если совсем никак не влезает, бьём на две отправки, и т.п.

Это на передающей стороне. А что делать на приемной? Вот заполнился приемный буфер, а данные из линии продолжают сыпаться…

Да в некоторых протоколах у приемника бывает возможность управления потоком — как-то попросить передатчик приостановить передачу. Но далеко не во всех.
Значит, мы не успеваем обрабатывать данные в том темпе, в котором они поступают, и у нас проблема.
Вообще, я привёл один из многих примеров, когда «антипаттерн», а котором так категорично рассуждает автор, совсем не является проблемой, а скорее, наоборот.
Вы же мне предлагаете рассмотреть вообще все возможные случаи, когда приведённый мною пример не работает?
Не существует какого-то универсального решения или подхода, который был бы абсолютно правильным. Мир разнообразен, и в каждом случае наиболее подходящее решение будет своё. Более того, одному разработчику в данном конкретном случае кажется правильным одно решение, другому — совсем другое. И это нормально.
А вот категорично рассуждать, подобно автору, о том, что вот данное решение всегда неправильное, я бы не стал.
С МК может быть еще веселее — что делать, если сообщение не помещается во всю память?
Держаться молодцом, пить водку, думать об архитектуре и покупать другой МК.
Думать об архитектуре раньше надо было. Зачастую — еще позапрошлому программеру.

Другой МК решение, да. Но это значит и вносить (и согласовывать) изменения в конструкцию, план закупок и т.п. Но это еще фигня.

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

Описанный вами сценарий очень напоминает анекдот про сибирских мужиков и японскую бензопилу или историю одного байта. Попытались в новом обновлении, которое, вероятно, уже продали, впихнуть невпихуемое, а оно внезапно не влезло? Да, это проблема продукта. Но проблема ли это конечного программиста? Нет, теоретически можно посидеть, подумать, вынести по максимуму в статику, сделать для нескольких сообщений общую маску (вы наверняка не хуже меня знаете набор приёмов, как ужаться по памяти ценой времени исполнения и сложности отладки), но со временем проект будет выглядеть так, будто он написан на Malbolge, и поддержка станет или очень дорогой, или завяжется на одного человека, который понимает, как это, чёрт возьми, работает. Так что я бы отрубил хвост в один заход с перепроектированием.
В нашем случае так все в итоге и вышло. Сначала дикая комбинаторика с ручной оптимизацией — до «одного байта» далековато еще было (но байку эту периодически вспоминали — она так-то куда старее, чем публикация на EE, еще в ФИДО ходила), но в целом очень похоже. Ну ладно, в моём проекте когда программа (на асме), перестала помещаться во флеше, ее оказалось возможно как следует ужать, поскольку предыдущий программер писал очень рыхлый код (заодно и код проще стал). А вот коллеги на другом проекте уперлись в RAM, и оптимизировали размещением в одной и той же ячейке разных переменных, используемых в разное время (ну понятно, что тут могдо пойти не так — про одну такую переменную забыли, что она делит место с другой, и использовали ее в прерывании...).
Правда, хоть проблем с обратной совместимостью не было, так что переползти на другие контроллеры оказалось легко (с архитектуры mcs51 на AVR).

На противоречие «новые фичи vs обратная совместимость» напоролись уже в другом случае, хотя и без замены контроллера. И начальство выбрало новые фичи.

Но напоминаю, эта ветка комментов началась с «просто выделяю буфер с запасом» и вопроса «а как быть если новое сообщение не помещается в буфер». Тут я, конечно, несколько додумал возможный ответ «еще увеличить буфер» и намекнул, что увеличение буфера довольно быстро упирается в конечный размер памяти, каковой обычно в МК очень небольшой.

Конечно, на этапе проектирования это еще вообще не проблема — вполне можно проанализировать, а какой самый большой размер сообщения надо обрабатывать — и соответственно выбрать контроллер (или добавить внешнюю память). Проблемы начинаются именно когда надо внести доработки по хотелкам заказчика. И даже выбор «с запасом» плохо помогает. Ну сколькикратный запас можно заложить? пятикратный? десятикратный? А ведь может внезапно всплыть, что если раньше сообщения ограничивались сотнями байт, то в новом протоколе могут оказаться и десятки килобайт (под длину выделено уже два байта). Да, в итоге вполне можно прийти к какому-то решению — от оптимизации архитектуры обработки до замены элементной базы. Но в первый момент, когда такое требование приходит — возникает естественная реакция схватиться за голову и произнести короткое «антидепрессивное заклинание».

проблема ли это конечного программиста
Зависит от стиля руководства. Если «начальник сказал — хорёк! и никаких сусликов», то или как-то решаешь эту проблему или остаешься без премии. Ну или плюёшь на такие закидоны и идешь искать другую работу с адекватным начальством (что может оказаться куда большей проблемой).
Вообще я согласен с утверждение «просто увеличьте буфер», но так можно дойти и до предусматривания посадочных мест под внешнюю память. Если чуть серьёзнее, то я считаю традицию разрабатывать встраиваемое ПО по водопадоподобной методологии с обязательным ТЗ правильной. Иными словами, значительное изменение ТЗ означает, что мы не дорабатываем старый продукт, а делаем новый. Возможно, на базе старого и даже обратно совместимый по железу (если мы взяли МК не за 8, а за 10 баксов, чтобы вкорячить туда РТОС именно с целью расширения функционала), но и заказчику, и разработчику на неизменность железа надеется не стоит.
Да, водопад, согласование ТЗ на доработки — всё было. Но и это не спасает если ТЗ именно на доработку существующего продукта. «Вот пусть будет то же самое, но с перламутровыми пуговицами! Но только то же самое!»

Заказчику-то, надо сказать, обычно плевать, какой контроллер внутри и вообще содержимое «коробочки». Его волнует функциональность и таки взаимозаменяемость — чтобы можно было новую коробочку взять и поставить вместо старой, и при этом не перелопачивать свою систему.

И да, «нахрен всё, делаем новый продукт с отличающимся обозначением, уговорив заказчика» у нас тоже было. Поскольку позволить установить вместо новой старую коробочку со склада тоже нельзя — иначе возникают претензии «ваша железка не работает как надо!».

И посадочные места под внешнюю память «на будущее» мы как-то тоже делали… Наряду с местами под цепи дополнительных входов и выходов. На случай «вдруг еще что заказчику в голову взбредет, а у нас на складе запас плат не израсходован». Когда заказчик — большой завод, а ты — довольно мелкая компания, приходится плясать перед ним на цыпочках. Поскольку другую такую контору по разработке он найдет, а вот мы другой такой завод — нет. Так что вполне можно понять руководство, которое заглядывает заказчику в рот и по любому его почесыванию «а неплохо бы вот еще так сделать» берет под козырек и спускает ТЗ отделу разработки. А заказчик потом «а не, так не надо, давайте по-другому»… ну да ладно, хватит о грустном.

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

В общем, возвращаясь к теме — суть в том, что разработка на МК все же куда ограниченнее в применении экстенсивных методов. На ПК запас под «увеличение буфера» и т.п. находится в подавляющем большинстве случаев, изменение объемов постоянно хранимой информации тоже довольно легко масштабируется (хотя пределы тоже есть, но подавляющее большинство задач в них влезает со свистом).
Я понимаю Вашу боль, но помочь ничем не могу :) Единственное, что можно придумать при вытанцовках перед заказчиком, так это вывести наружу UART/USB, чтобы когда перестанет хватать мощностей, цеплять вторую-третью-следующую платы для кооперативной работы. Так делает Ludl: материнка с универсальными разъёмами, куда ставятся «блейды». «Паровоз» на UART я тоже видел. но сходу не вспомню. Вроде штучная разработка, там тоже случилась быль о маленьком звездолёте
суть в том, что разработка на МК все же куда ограниченнее в применении экстенсивных методов
С этим и не собирался спорить :)
Во времена оны, одни мои знакомые сгородили PCI-железяку, которая должна была принимать из внешнего источника пару сотен килобайт в секунду и столько же писать туда, причём чётко по таймингам. Всё бы ничего, но они туда поставили mega64 с 4 КБ RAM, из которых свободно получалось 2 килобайта. Это я уже молчу, что скорость «меги» 16 МГц и путём несложных подсчётов можно выяснить сколько тактов приходится на обработку одного байта данных.
А я вот с примером с сообщением не согласен, что это вредно.

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

Приведенный выше код столь же ужасен с точки зрения обслуживания, как:
Совершенно не согласен.
Ключевая разница — в том, что второй кусок данных константный, в то время как данные в массиве myArray могут быть модифицированы (и, по-нормальному, должны быть — иначе и их лучше сделать константными).
Алгоритмы вида x[n+2]=a*x[n+1]+b*x[n] встречаются сплошь и рядом.
Не совсем понял, почему «анти». Это стандартный подход из С, когда мы сначала преаллоцируем память, а потом используем arrSize для итерирования.
Поэтому я настоятельно предпочитаю T[] более новому std::array<T, N>.
std::array всё еще надежнее как минимум потому, что не приводится неявно к указателю и нормально копируется/мувится. И бонусом сверху есть некоторый синтаксический сахар, типа операторов лексикографического сравнения, structured bindings и пр.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий