Что: 51fe13c5032ecccbb838fae2f33541c202301bba
Когда: 2020-12-24 12:51:21+03:00
Темы: c go hate
Что ненавижу в Си 1) Когда записывают if/for без фигурных скобок. Да, это 1-2 строки экономит, но когда надо вставить ещё какую-нибудь команду, например print для отладки, то начинаются пляски с добавлением. А ещё не всегда замечаешь что фигурных скобок нет, вставляешь print, и только он после if срабатывает, а штатная, следующая после него команда, уже находится вне if. 2) В целом ненавижу как возвращается успех/не успех выполнения функи. Такое впечатление, что половина людей/фунок считают int=0 успехом выполнения, а половина int≠0, и ещё меньше нуля отдельная категория. И, как правило, по названию функи не поймёшь что от неё ожидать. Вот если cmp, то скорее всего 0 означает равенство. Вот только фиг -- есть и исключения. Если функа называется is_equal, то точно стоит ожидать что возвращает 1 при равенстве, чтобы можно было записать if(is_equal), но это редкость встретить такие говорящие названия. Ну и лично я, когда вижу, восклицательные знаки, то это ассоциируется с чем-то негативным, отрицательным, хотя 100500 мест уже видел где if (!...) это проверка на успешность выполнения. Плюс бесит отсутствие ЯВНЫХ указаний что ожидается от проверки (==0, >-1, !=0, и т.д.) -- но это касается также и кода для Python, где видел тьму ошибок совершаемых из-за этого. А ведь в Unix return code = 0 это успех, хотя в Сях оно eval-ится в false. После этого понимаю насколько Go всё же молодец тем, что if проверяет только и только булевы выражения, а не эти неявные преобразования в них. Если какие-то функи возвращают NULL, то это значит что не успех. А в чём ошибка? Или глобальные переменные смотри или в, указанную отдельным аргументом, переменную с результатом смотри. Или >0 -- успех, ==0 -- такая-то ошибка, <0 -- другие такие-то ошибки. После этого особо стал ценить задумку с error типом в Go. Вообще в Сях я по сути пишу как на Go. Практически все функи возвращают структуру которая как-бы является error-ом, внутри которого есть .code возможно говорящий что ошибки не было. Все функи возвращают error этот, почти без исключений. Это и возможность кучу дополнительной информации передать сопутствующей ошибки. Код везде становится очень простым из серии: err=MyFunc, if(HasErr(err)). Ещё я всюду и везде делаю не просто указатели на массивы, но и рядом с ними size_t размер данных. А в идеале вообще можно и нужно бы было делать struct из указателя и размера, по сути делая недо-slice из Go. Для входных read-only данных это const size_t, а для выходных это size_t*, куда записывается кол-во данных записанных, или возвращается ошибки и записывается сколько данных в dst нужно иметь, но не хватает. Особенно видя OpenSSL код, я ужасаюсь кучей потенциальных проблем которые может вызвать всё это отсутствие указания явных размеров и как нужно доверять разработчику. Собственно, понимаю что с таким кодом ненавидеть Си -- благое дело и здоровая реакция. Отдельная боль это конечно очистка данных при выходе из функи, при ошибках. Если в Go хотя бы есть garbage collector, то в Си нужно не забывать освобождать память. В Go хотя бы есть defer или, как минимум, анонимные функции. В Сях нужно очень аккуратным быть. Я делаю int needCleanup = 0 переменную, а дальше с каждым действием требующим "очистки"/освобождения, её инкрементирую (if (malloced == NULL); needCleanup++). А при очистке декрементирую (free(...); needCleanup--) и вставляю assert(needCleanup == 0) перед каждым return-ом. Костыль, недоверие к самому же себе, но этот подход не раз уже окупился у меня на практике.
From: kmeaw Date: 2020-12-24 10:43:01Z > Я делаю > int needCleanup = 0 переменную, а дальше с каждым действием требующим > "очистки"/освобождения, её инкрементирую А можно чуть подробнее? Основная проблема у меня и с Go, и с C в освобождении ресурсов при ошибках. Не хватает исключений и RAII из C++. С памятью обычно всё проще - если кусок небольшой, то его можно выделить на стеке, а в happy path выделить на куче, скопировать и вернуть. А если выделять можно не всегда, то удобно пользоваться тем, что free(NULL) - это no-op, а realloc(NULL, sz) эквивалентен malloc(sz). И в зависимости от применения надо либо оборачивать все аллокации в alloc-or-abort, либо очень внимательно копировать временные указатели при использовании realloc, чтобы не затереть единственную ссылку NULL'ом. А вот с файлами, namespaces, сетевыми интерфейсами и прочими объектами ОС всё сильно сложнее. Чаще всего делаю примерно так же, как делают в Linux: Err make_res_and_do_work() { res1 = get_res1(); if (res1 == NULL) { err = error("cannot get res1"); goto fail_res1; } res2 = get_res2(); if (res2 == NULL) { err = error("cannot get res2"); goto fail_res2; } err = do_work(res1, res2); fail_res2: put_res2(res2); fail_res1: put_res1(res1); return err; } defer это, конечно, хорошо, но бывают случаи, когда вызываемая функция передаёт владение объектом вызывающей, и нужно реализовать стратегию "всё или ничего" для выделяемых ресурсов.
From: Sergey Matveev Date: 2020-12-24 11:10:01Z
Сгенерирован: SGBlog 0.34.0