ljr - Юра Бронников - Чистый C для C++-ника [entries|archive|friends|userinfo]
Юра Бронников

[ userinfo | ljr userinfo ]
[ archive | journal archive ]

Чистый C для C++-ника [Sep. 19th, 2012|01:20 am]
Previous Entry Add to Memories Tell A Friend Next Entry
На работе сейчас мне приходится в основном писать на языке C++. Я, видимо, настолько громко ругался по этому поводу, что один коллега меня спросил, не могу ли я ему посоветовать текст, как для программиста, привыкшего к плюсам, перейти на чистый C. Я пошарил по сети, ничего в этом роде не нашел и решил написать такой ликбезик сам. Возможно, он будет пополняться и улучшаться со временем; с радостью учту ваши комментарии.
Разумеется, многое здесь для приличного C-программиста будет очевидным; я прошу прощения за тривиальность. С другой стороны, если мои собственные предпочтения расходятся с общепринятой практикой, я тоже буду это указывать.
Вопрос, почему переходить именно на C, а не на какой-нибудь более продвинутый язык1, здесь не ставится2. Итак:
    1. Книга Кернигана и Ритчи великолепна. Если Вы ее еще не прочитали, сделайте это.
    2. Помимо нее, есть различные style guides. Вот, например, рекомендации Пайка; а вот правила оформления кода в ядре Linux.
    3. Наконец, вокруг много хорошо написанного кода (например, многие опенсорсные библиотеки). Этот код полезно читать и имитировать.
  1. После того, как я пережил увлечение C++, мой код на C стал лучше. ООП — небесполезный набор методик и эвристик. При отказе от C++ необязательно забывать их все.
    1. Для каждой структуры желательно иметь функцию-фабрику, которая ее выделяет и инициализирует, и функцию-деструктор, которая освобождает ресурсы. Имена функций, работающих со структурой, должны иметь соответствующий префикс.
      qz_t *make_qz(int a, float b);
      void qz_free(qz_t *qz);
      
      int qz_foo(qz_t *qz, qq_t *qq, int n);
      
    2. Как только возникает потребность в полиморфном (в ООП смысле) поведении, полезно завести структуру-"поведение" из указателей на функции, и держать ее первым полем у соответствующего типа. При этом указатель на структуру-"потомок" можно спокойно преобразовывать в указатель на структуру-предок (а внутри "методов" — обратно).
      struct qz_ops {
         int (*foo)(qz_t *qz, int a);
         double (*bar)(qz_t *qz, char *s);
      };
      
      struct qz {
        qz_ops *ops;
      }
      
      struct qz_ops qqq_ops = { // стиль объявления C99
        .foo = qqq_foo,
        .bar = qqq_bar
      };
      
      struct qqq {
        qz_t qz;
        int other_field;
      }
      
      и потом
      qz_t *q = make_qqq(); // проинициализирует qqq и установит поле ops
      q->ops->foo(q, 33);
      
      Грубо говоря, эти два правила имитируют C++-классы на коленке, и оказывается, что весь плюсовый синтаксический сахар повышает удобство очень ненамного.
    3. Желательно активно пользоваться ограничением видимости. То есть, почти любой тип определяется в хедере просто как
      typedef struct qz qz_t;
      
      в публичных функциях (перечисленных в том же хедере) используется qz_t*, а реальное определение структуры сидит уже в C-шнике. При этом все неэкспортируемые через хедер функции в C-шном файле обозначены как static.
      В результате, в отличие от C++, заголовочные файлы на чистом C вполне можно читать подряд; мусора, касающегося деталей реализации, там нет или почти нет.
      Это правило вступает в противоречие с предыдущим: если мы устраиваем полиморфную структуру, то, по крайней мере, ее член ops должен быть виден извне.
  2. Желательно взять какую-нибудь стандартную библиотеку со структурами данных. Я пользовался glib, причем в основном ее низкоуровневой частью (то есть, например, списки и хэши оттуда, а какие-нибудь GString или объекты -- нафиг). Но примерно ко времени, когда на предыдущей работе стал активно перелезать на Scheme, уже хотелось поискать что-то другое. Вроде в проекте Apache есть базовая библиотека APR. Я никогда с ней не работал, но выглядит как разумная альтернатива. Имеет смысл посмотреть и выбрать.
  3. Я сначала пользовался своей доморощенной библиотечкой со счетчиками ссылок, потом перелез на Boehm gc. (для пользования Boehm есть свой набор правил, чтобы получалось безопасно и эффективно).
    Использовать сборку мусора, разумеется, можно только в том случае, если Вы пишете приложение или такую библиотеку, которая может навязать правила обращения с памятью своим клиентам. В противном случае придется отслеживать указатели самим. Желательно в библиотеке позволять пользователю задать свои malloc/free (разумеется, через указатели).
    Так или иначе, иметь продуманную политику работы с памятью надо обязательно.
    1. Исключений, понятно, нет. Можно либо возвращать ошибку, если функция исходно была бы void, либо передавть указатель на ошибку отдельным аргументом:
      qq_t* qz_make_qq(qz_t *qz, int a1, char *a2, err_t **err);
      
      и потом
      {
        err_t *err = NULL;
        qq_t *qq;
      
        qq = qz_make_qq(qz, 33, "xxx", &err);
        if (err) {
          ...
        }
      }
      
      Это, надо сказать, не слишком красиво. Очень жаль, что в C нет множественных возвращаемых значений.
    2. Обрабатывать ошибки можно так:
      int foo() {
        int fd1 = -1;
        int fd2 = -1;
        int fd3 = -1;
      
        fd1 = open(...);
        if (fd1 < 0) goto OUT;
      
        fd2 = open(...);
        if (fd2 < 0) goto OUT;
      
        fd3 = open(...);
        if (fd3 < 0) goto OUT;
      
        ...
      
      OUT:
        close(fd1);
        close(fd2);
        close(fd3);
      }
      
      При этом деструктороподобные функции должны справляться с -1 или NULL. (close и free это умеют, кстати.)
  4. Лично я в своем коде не использовал модификатор const. По-моему, он очень коряво встроен в общую систему типов. То есть, правила, по которым возможны/невозможны присваивания, например, между типами
    int (*)(int **);
    int (*)(int *const *);
    int (*)(int const **);
    int (* const)(int const *const *);
    
    и пр. — эти правила, конечно, оправданы, но отслеживать правильность всех подобных типов мне оказывается тяжелее, чем вылавливать относительно редкие ошибки из-за того, что что-то лишнее присвоилось переменной, которая должна была быть const. И первый из этих типов читается проще, хоть и дает наименьшие гарантии.
    С другой стороны, проблема таки реальная, так что я рекомендую эту политику не "вообще", а просто как деталь моего собственного вкуса.
    Кстати, в C, поскольку передача параметров только по значению, часто сигналом модифицируемости оказывается амперсанд перед параметром. Т.е., в функции
    int foo1(qz_t *qz);
    
    qz не поменяется (хотя может измениться содержимое памяти по указателю). по крайней мере, он останется "тем же" объектом. А в
    int foo2(qz_t **qzp);
    
    c qzp может случиться что угодно, и это видно по вызову foo2(&qz).
  5. Вообще-то C позволяет передавать структуры по значению в функции и возвращать их как результаты, а также присваивать их друг другу. Но лично я стараюсь, чтобы всякое присваивание и передача параметра в моих программах приводили к пересылке одного слова и не больше. То есть, если есть
    struct qz {
      int i;
      double d;
    };
    
    struct qz a, b;
    
    то я вместо
      a = b;
    
    напишу уж лучше
      memmove(&a, &b, sizeof(a));
    
    Мне так читать код оказывается проще.
  6. В уже упомянутых рекомендациях Пайка, среди прочего, есть замечательное и очень нестандартное правило `include files should never include include files'. Я его в свое время у себя в проектах принял и не пожалел. Т. е., вместо
    #ifndef qz_H
    #  define qz_H
    ...
    #endif // qz_H
    
    я писал
    #ifdef qz_H
    #  error "qz.h doubly included"
    #endif
    #define qz_H
    
    То есть, поскольку в C (и C++) нет никакой разумной системы модулей, правильнее заставлять программиста в начале каждого C-файла явно перечислять все зависимости.
    Но это практика и для языка C очень необычная. Я бы не решился ее применять в библиотеке, если я не властен над стилем ее пользователей.

1 Для меня это будет, в зависимости от задачи и настроения, один из Go, OCaml, Haskell, Scheme, Python или Erlang.
2 Можно, например, считать, что мы пишем переносимый код для нескольких сразу мобильных платформ, и усилий по перетаскиванию продвинутой среды слишком много, а сама среда отъест неприемлемо много места.
Или требуется написать библиотеку, чей интерфейс будет доступен из многих высокоуровневых языков (
[info]yushi, спасибо за напоминание!).
LinkLeave a comment

Comments:
[User Picture]
From:[info]yushi
Date:September 27th, 2012 - 12:01 pm
(Link)
ничего в этом роде не нашел

Только что попалось на глаза: http://www.intuit.ru/department/se/ialgdate/14/

Хотя, конечно, твой текст полезнее и обстоятельнее.

Что до сфер применения C, странно, что ты забыл, наверное, главную — написание независимой от языка библиотеки. Оно, конечно, для C++ есть SWIG, но сильно не на всякий язык адекватно отображаются плюсовые абстракции, и для ещё меньшего количества языков это конкретно в SWIG сделано красиво.
[User Picture]
From:[info]gogabr
Date:September 27th, 2012 - 12:41 pm
(Link)
Да, странно, что забыл.

Текста по ссылке я при поиске не видел; наверное, потому что искал в основном англоязычное. Но он написан для другого: пытается определить область применения (на мой взгляд, слишком узко) и быстро-быстро перечисляет технические отличия от плюсов.

Я же пытаюсь для человека, который уже решил совершить переход, объяснить, как это сделать безболезненно и комфортно себя чувствовать в новой среде. Я подумываю, стоит ли отдельно написать мотивировку -- почему переходить с плюсов на Си правильно. Но не хочется ввязываться во флэйм; нет сейчас настроения.