[О блоге] [наверх] [пред] [2020-12-24 12:51:21+03:00] [51fe13c5032ecccbb838fae2f33541c202301bba]
Темы: [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-ом. Костыль,
недоверие к самому же себе, но этот подход не раз уже окупился у меня на
практике.

    [оставить комментарий]
    комментарий 0:
    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 это, конечно, хорошо, но бывают случаи, когда вызываемая функция
    передаёт владение объектом вызывающей, и нужно реализовать стратегию
    "всё или ничего" для выделяемых ресурсов.
    
    комментарий 1:
    From: Sergey Matveev
    Date: 2020-12-24 11:10:01Z
    
    *** kmeaw [2020-12-24 13:37]:
    >Чаще всего делаю примерно так же, как делают в Linux:
    
    Да, это самое что я видел из того что на практике применяют. Этот кусок
    кода я бы написал вот так:
    
        Err make_res_and_do_work() {
            int needCleanup = 0;
            res1 = get_res1();
            if (res1 == NULL) {
                assert(needCleanup == 0);
                return error("cannot get res1");;
            }
            needCleanup++;
            res2 = get_res2();
            if (res2 == NULL) {
                put_res1(res1);
                needCleanup--;
                assert(needCleanup == 0);
                return error("cannot get res2");
            }
            needCleanup++;
            err = do_work(res1, res2);
            put_res2(res2);
            needCleanup--;
            put_res1(res1);
            needCleanup--;
            assert(needCleanup == 0);
            return err;
        }
    
    а если бы это был криптографический ключ, то:
    
        uint_8 *key = malloc(...);
        if (key == NULL) {...};
        needCleanup++;
        memcpy(key, src, srcLen);
        needCleanup++; // для того чтобы не забыть его обнулить
    
    Я совершенно не уверен в правильности и простоте своего "решения", но
    лично мне пока с ним удобнее и проще всего. goto подход мне очень не
    нравится тем, что мне сложнее понимать где как и что надо расставлять в
    плане очистки. Плюс очень часто вижу что при goto подходе у людей
    сплошные if (something != NULL) free(something) появляются, а не явная
    очистка происходит. Мне приятно осознавать что тут явно ожидается
    "грязный" ресурс и его явно нужно очистить. А с if-ом для меня это
    читается как "фиг его знает, может он не проинициализирован, может
    быть уже ранее очищен, но если что, очисть". *Как будто* нет чёткого
    понимания у автора. А в "моём" подходе я чётко вижу что и как очищается
    на каждом return-е. Если появляется что-то новенькое в коде, то мне
    нужно просто ниже этого участка пройти по всем return-ам и добавить в
    перед ними очистку и декремент needCleanup. Вызов free(NULL) мне не
    приятен тем, что я не могу интерпретировать эту строку как "вот тут мы
    освобождаем память", а приходится как "если она ещё не освобождена, то
    сделать это" -- нет чёткости понимания. Повторюсь, всё это лично мои
    ощущения. В "моём" подходе может быть значительно больше кода, ибо чуть
    ли не на каждой строке возможна проверка на ошибку и после каждой
    проверки может быть портянка на дюжину строчек с чисткой и
    needCleanup--. Но мне кажется это более чёткий и понятный код. Возможно
    в будущем я изменю своё мнение, опыта то программирования на Сях у меня
    с полгода.
    
    >defer это, конечно, хорошо, но бывают случаи, когда вызываемая функция
    >передаёт владение объектом вызывающей, и нужно реализовать стратегию
    >"всё или ничего" для выделяемых ресурсов.
    
    Безусловно. defer не панацея и не всегда удобен. А раньше ещё и overhead
    имел очень и очень весомый и я defer только для не часто вызываемых
    фунок использовал. Если defer в Go не использую, то аналогично как и в
    Сях перед return-ами просто стараюсь не забывать расставлять очистку.
    assert-ов и needCleanup не делаю, так как не часто бывает нужно очищать
    много всего и сразу, тогда как в Сях регулярно.