«Code Monkey like Fritos
Code Monkey like Tab and Mountain Dew
Code Monkey very simple man
With big warm fuzzy secret heart:
Code Monkey like you
Code Monkey like you»

— Jonathan Coulton — Code Monkey


Я думаю, многим знакома эта шикарная песня Jonathan Coulton'а, и эта жизненная ситуация, когда «Rob say Code Monkey very diligent», но «his output stink» и «his code not 'functional' or 'elegant'».

Язык Си, подаривший нам столько полезного софта, потихоньку был вытеснен из десктопа и энтерпрайза такими высокоуровневыми гигантами как Java и C# и занял нишу системного программирования. И все бы хорошо, но системщики — очень отбитые своеобразные ребята. Задачи, которые порой возникают перед ними даже своей формулировкой способны вогнать в ужас простых смертных. Собственно говоря, так же, как и некоторые решения.

Сегодня мы поговорим о некоторых полезных практиках, которые я вынес из глубин системного программирования на Си. Поехали.

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

Практика I: Соблюдайте единый Code Style и фундаментальные принципы «хорошего тона»


Функция принимает в качестве аргумента переменную INPUT, парсит её в массив IncomingValues и возвращает result_to_return? Отставить быдлокод!

То, что в первую очередь выдает новичка — несоблюдение единого стиля написания кода в рамках конкретного приложения. Следом идет игнорирование правил «хорошего тона».

Вот несколько самых распространенных рекомендаций к оформлению кода на Си:

  • Названия макросов и макрофункций пишутся капсом, слова в названиях отделяются друг от друга нижним подчеркиванием.

    #define MAX_ARRAY_SIZE    32
    #define INCORRECT_VALUE   -1
    #define IPC_FIND_NODE(x)  ipc_find_node(config.x)
    

  • Названия переменных записываются в нижнем регистре, а слова в названиях отделяются нижним подчеркиванием

    int my_int_variable = 0;
    char *hello_str = "hello_habrahabr";
    pid_t current_pid = fork();

    Вообще, этот пункт спорный. Мне доводилось видеть проекты, где имена переменных и функций пишутся в camelCase и PascalCase соответственно.

    UPD: Спасибо пользователю fogree за то, что он обнаружил косяк с перепутанным PascalCase и camelCase.

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

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

    То же можно сказать и про переменные — никаких a, b, c — в названии должен быть отражен смысл (итераторы не в счет). Самодокументируемый код — очень хорошая практика.

  • Специализированные функции (которые вызываются в пределах работы внутри какого-то специфичного контекста) лучше называть так, чтобы было определенно ясно, что делает эта функция.

    Как правило, можно выбрать между стилем написания названия: PascalCase и under_score, тут уже зависит от вас.

    /* пример функции общего пользования */
    static void dgtprint(char *str) {
         int i;
         for (i = 0; i < strlen(str); i++) {
             if (isdigit(str[i]))
                 printf("%c", str[i]);
             else
                 print("_");
         }
    }
    
    /* пример функции для работы со специфичным контекстом */
    /* PascalCase */
    void EnableAllVlans(struct vlan_cfg *vp) {
        int i;
        for (i = 0; i < VLAN_COUNT; i++) {
            EnableVlanByProto(vp.vlan[i]);
        }
    }
    
    /* under_score */
    void enable_all_vlans(struct vlan_cfg *vp) {
        int i;
        for (i = 0; i < VLAN_COUNT; i++) {
            enable_vlan_by_proto(vp.vlan[i]);
        }
    }

  • i, j, k — стандартные названия для итераторов цикла

    int array[MAX_ARRAY_SIZE] = arrinit();
    register int i, j, k;
    for (i = 0; i < MAX_ARRAY_SIZE; i++) 
        for (j = 0; j < MAX_ARRAY_SIZE; j++)
            for (k = MAX_ARRAY_SIZE; k >= 0; k--)
                dosmthng(i, j, k, array[i]);

  • Соблюдайте однородность переноса скобок

    if (condition) { dosmthng(); } else
    {
        dont_do_something();
    } /* Не делайте так */
    
    if (condition) {
        dosmthng(); 
    } else {
        dont_do_something();
    } /* Гораздо правильнее будет следовать одному правилу переноса скобок, как тут */
    
    if (condition) 
    {
        dosmthng(); 
    } 
    else
    {
        dont_do_something();
    } /* Или как тут */
    
    /* Ну, или как тут, но это уже совсем экзотика */
    if (condition) { dosmthng(); } else { dont_do_something(); } 

  • Объявляйте переменные в начале функции. Если это глобальные переменные, то в начале файла.

    По возможности инициализируйте переменные при объявлении. Численные с помощью нуля, указатели — NULL:

    int counter = 0, start_position = 0, unknown_position = 0;
    struct dhcp_header * dhcp = NULL, * dhcp_temp = NULL;
    char input_string[32] = { 0 };

    Ну оставили мы переменные неинициализированными, и что?

    А то. Если смотреть их (до инициализации) в отладке (в том же gdb), там будет лежать мусор. Это нередко сбивает с толку (особенно, если мусор «похож на правду»). Про указатели я вообще молчу.

  • Пишите комментарии с умом.

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

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

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

    /* Возвращает 1, если параметры, связанные с модемным 
     * соединением изменились, и 0, если нет. 
     */
    static int CheckModemConnection()
    {
      int i = 0;
      /* Проверка сети отдельно - меняется чаще всего */
      if (CHECK_CFG_STR(Network.LanIpAddress) || CHECK_CFG_STR(Network.LanNetmask))
        return 1;
    
      for(i = 0; i < MAX_MODEM_IDX; i++)
      {
        if (CHECK_CFG_INT(Modems.Modem[i].Proto) || CHECK_CFG_INT(Modems.Modem[i].MTU) ||
          CHECK_CFG_STR(Modems.Modem[i].Username) || CHECK_CFG_STR(Modems.Modem[i].Password) ||
          CHECK_CFG_STR(Modems.Modem[i].Number) || CHECK_CFG_STR(Modems.Modem[i].AdditionalParams) ||
          CHECK_CFG_STR(Modems.Modem[i].PIN) || CHECK_CFG_STR(Modems.Modem[i].MRU) || 
          CHECK_CFG_STR(Modems.Modem[i].PppoeIdle) || CHECK_CFG_STR(Modems.Modem[i].USBPort) ||
          CHECK_CFG_STR(Reservation.Prefer) || CHECK_CFG_STR(Modems.Modem[i].PppoeConnectType) || 
          CHECK_CFG_INT(Modems.Mode) || CHECK_CFG_INT(Aggregation.usb1) || CHECK_CFG_INT(Aggregation.usb2))
          return 1;
      }
      return 0;
    }

    Если вы постоянно работаете с трекерами (вроде RedMine), то при внесении правок в код можно указать номер задачи, в рамках которой эти правки были внесены. Если у кого-то при просмотре кода возникнет вопрос а-ля «Зачем тут этот функционал?», ему не придется далеко ходить. В нашей компании еще пишут фамилию программиста, чтобы если что знать, к кому идти с расспросами.
    /* Muraviyov: #66770 */



P.S. Для тех кто устраивается на работу: так же не следует забывать, что в каждой компании, как правило, используется свой Code Style, и ему нужно следовать. В противном сулучае можно получить как минимум укоризненные взгляды товарищей-разрабов или втык от начальства.

Практика II: Оптимизируйте структуру вашего проекта


Если у вас в проекте несколько файлов — имеет смысл хорошо подумать над структурой проекта.
Каждый проект уникален, но, тем не менее, существует ряд рекомендаций, которые помогут удобно структурировать проект:

  1. Называйте файлы так, чтобы всем было ясно, какой файл за что отвечает.

    Не следует называть файлы file1.c, mySUPER_COOL_header.h и т.д.
    main.c — для файла с точкой входа, graph_const.h — для заголовочника с графическими константами будет в самый раз.

  2. Храните заголовочники в директории include.

    Рассмотрим пример:

    • project/
      • common.c
      • common.h
      • main.c
      • network.h
      • networking.c
      • networking_v6.c
      • packet.c
      • packet.h
      • Makefile

    В принципе, проект как проект. Но давайте на секунду представим, что у нас не 9 файлов, а, скажем, 39. Что-то быстро найти будет проблематично. Да, в консоли — пара пустяков, но что если человек работает с GUI, или, что еще хуже, пытается найти файл в Github/Gitlab/Bitbucket?

    Если он точно не знает, какой файл ему нужен? Можно сберечь много нервов, если сделать так:

    • project/
      • include/
        • common.h
        • network.h
        • packet.h

      • common.c
      • main.c
      • networking.c
      • networking_v6.c
      • packet.c
      • Makefile

    Не забываем, что путь к директории include следует указать в параметрах сборки. Вот примерчик для простого Makefile (include в той же директории, что и Makefile):

    @$(CC) $(OBJS) -o networkd -L$(ROMFS)/lib -linteraction -Wall -lpthread -I ./include

  3. Логически группируйте .c файлы в папки.

    Если у вас игра, в которой есть файлы, отвечающие за движок/звук/графику — будет удобно раскидать их по папкам. Звук, графику и движок — отдельно друг от друга.

  4. Дополнение к предыдущему пункту — файлы сборки круто размещать в каждой из таких отдельных директорий, и просто вызывать их из файла сборки в корневой директории. В таком случае Makefile в корневой директории будет выглядеть примерно так:

    .PHONY clean build
    build:
        cd sound/ && make clean && make 
        cd graphics/ && make clean && make
        cd engine/ && make clean && make
    sound:
        cd sound/ && make clean && make
    graphics:
        cd graphics/ && make clean && make
    engine:
        cd engine/ && make clean && make
    clean:
        cd sound/ && make clean
        cd engine/ && make clean
        cd greaphics/ && make clean

Практика III: Используйте враппер-функции для обработки возвращаемых значений


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

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

Но обрабатывать значение от каждой функции в коде — такое себе решение. Тут же упадет читаемость, и объем (+ избыточность) кода увеличится в пару раз.

Тут и помогают врапперы. Рассмотрим первый пример — безопасный код без врапперов:

int sock_one = 0, sock_two = 0, sock_three = 0;
/* операция сравнения имеет больший приоритет, чем операция присваивания, 
 * поэтому присваивание выполняется в скобках
 */
if ((socket_one = socket(AF_INET , SOCK_STREAM , 0)) <= 0) { 
    perror("socket one");
    exit(EXIT_ERROR_CODE);
}
if ((socket_two = socket(AF_INET , SOCK_DGRAM , 0)) <= 0) { 
    perror("socket two");
    exit(EXIT_ERROR_CODE);
}
if ((socket_three = socket(PF_INET , SOCK_RAW , 0)) <= 0) { 
    perror("socket three");
    exit(EXIT_ERROR_CODE);
} 

Ну, такое себе, не правда ли? Теперь попробуем с обертками.

/* Где-то в коде... */
int Socket(int domain, int type, int proto) {
    int desk = socket(domain, type, proto);
    if (desk <= 0) {
        perror("socket");
        exit(EXIT_ERROR_CODE);
    }
    return desk;
}
/* ......... n строчек спустя - наш предыдущий пример ......... */
int socket_one = 0, socket_two = 0, soket_three = 0;
socket_one = Socket(AF_INET , SOCK_STREAM , 0);
socket_two = Socket(AF_INET , SOCK_DGRAM , 0);
socket_three = Socket(PF_INET , SOCK_RAW , 0);

Как видите, код по-прежнему безопасен (не будет «немого» падения), но теперь его функциональная часть гораздо компактнее.

Я называю обертки именем самих функций, но с большой буквы. Каждый сам волен выбрать, как их оформлять.

В использовании оберток есть небольшой минус, который, если захотеть, можно решить костылем. А что это за минус — можете предположить в комментариях :)

Практика IV: Используйте keywords как профи


Хорошее знание keywords никогда не будет лишним. Да, и без них ваш код будет работать, не спорю. Но когда речь зайдет об экономии места, быстродействии и оптимизации — это именно то, чего вам будет не хватать.

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

  • register — дает компилятору указание по возможности хранить переменную в регистрах процессора, а не в оперативной памяти. Использование модификатора register при объявлении переменной-итератора цикла с небольшим телом может повысить скорость работы всего цикла в несколько раз.

    register byte i = 0;
    for (i; i < 256; i++)
        check_value(i);

  • restrict — при объявлении указателя дает компилятору гарантию (вы, как программист, гарантируете), что ни один указатель не будет указывать на область памяти, на которую указывает целевой указатель. Профит этого модификатора в том, что компилятору не придется проверять, не указывает ли какой-то еще указатель на целевой блок памяти. Если у вас внутри функции несколько указателей одного типа — возможно, он вам пригодится.

    void updatePtrs(size_t *restrict ptrA, size_t *restrict ptrB, size_t *restrict val);
    

  • volatile — указывает компилятору, что переменная может быть изменена неявным для него образом. Даже если компилятор пометит код, зависимый от волатильной переменной, как dead code (код, который никогда не будет выполнен), он не будет выброшен, и в рантайме выполнится в полном объеме.

    int var = 1;
    if (!var)             /* Эти 2 строчки будут отброшены компилятором */
        dosmthng();   
    
    volatile int var = 1;
    if (!var)            /* А вот эти  - нет */
        dosmthng();  

И это только вершина айсберга. Различных модификаторов и ключевых слов — куча.

Практика V: Не доверяйте себе. Доверяйте valgrind.


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

Valgrind — программа, которая создана для того, чтобы помочь программисту выявить утечки памяти и ошибки контекста. Не буду вдаваться в подробности, скажу лишь, что даже в небольших программах он нередко находит косяки, которые совсем не очевидны для большинства программистов, но, тем не менее, в эксплуатации могут повлечь за собой большие проблемы. За всем не уследишь.
+ у нее есть и другой полезный функционал.

Более подробно о нем можно узнать тут.

Практика VI: Помогайте тем, кто хочет улучшить ваш софт


Пример будет взят из исходников busybox 1.21. Для тех кто не знает, что такое busybox, можете посмотреть эту вики-статью.

UPD: до этого здесь был пример «плохого» кода из busybox. Спасибо пользователю themiron за то, что показал, что этот код был понят мною неправильно — это были лишь тонкости реализации, причем реализации очень хорошей. В качестве извинения за свою «клевету» на busybox, здесь будет пример хорошего кода.

Причем, все так же из busybox.

Код busybox очень эллегантен, пусть и совсем не прост. Всем, кто хочет взглянуть на язык си под другим углом — рекомендую ознакомиться с исходниками.

Теперь обобщения по этому пункту на примерах из busybox. Все примеры взяты из udhcpc — крохотного DHCP клиента:

  • Оставляй комментарии, там где они нужны.

    Протокол DHCP имеет полную документацию в RFC, там описаны все возможные поля dhcp-пакета. Но, тем не менее, ребята озаботились и полностью задокументировали даже поля структуры. Эта структура — первое, на что посмотрит программист, расширяющий функционал программы (DHCP-клиент <-> DHCP-пакет).

    (файл networking/udhcp/common.h)

    struct dhcp_packet {
    	uint8_t op;      /* BOOTREQUEST or BOOTREPLY */
    	uint8_t htype;   /* hardware address type. 1 = 10mb ethernet */
    	uint8_t hlen;    /* hardware address length */
    	uint8_t hops;    /* used by relay agents only */
    	uint32_t xid;    /* unique id */
    	uint16_t secs;   /* elapsed since client began acquisition/renewal */
    	uint16_t flags;  /* only one flag so far: */
    #define BROADCAST_FLAG 0x8000 /* "I need broadcast replies" */
    	uint32_t ciaddr; /* client IP (if client is in BOUND, RENEW or REBINDING state) */
    	uint32_t yiaddr; /* 'your' (client) IP address */
    	/* IP address of next server to use in bootstrap, returned in DHCPOFFER, DHCPACK by server */
    	uint32_t siaddr_nip;
    	uint32_t gateway_nip; /* relay agent IP address */
    	uint8_t chaddr[16];   /* link-layer client hardware address (MAC) */
    	uint8_t sname[64];    /* server host name (ASCIZ) */
    	uint8_t file[128];    /* boot file name (ASCIZ) */
    	uint32_t cookie;      /* fixed first four option bytes (99,130,83,99 dec) */
    	uint8_t options[DHCP_OPTIONS_BUFSIZE + CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS];
    } PACKED;
    

  • Держи однотипные и зависимые друг от друга вещи поблизости.

    Листинг выше частично вошел сюда, т.к. подходит для еще одного примера.

    Посмотрите: описание упакованных структур идет перед перечислением, отражающим размеры этих структур.

    (файл networking/udhcp/common.h)

    struct dhcp_packet {
    	uint8_t op;      /* BOOTREQUEST or BOOTREPLY */
    	uint8_t htype;   /* hardware address type. 1 = 10mb ethernet */
    	uint8_t hlen;    /* hardware address length */
    	uint8_t hops;    /* used by relay agents only */
    	uint32_t xid;    /* unique id */
    	uint16_t secs;   /* elapsed since client began acquisition/renewal */
    	uint16_t flags;  /* only one flag so far: */
    #define BROADCAST_FLAG 0x8000 /* "I need broadcast replies" */
    	uint32_t ciaddr; /* client IP (if client is in BOUND, RENEW or REBINDING state) */
    	uint32_t yiaddr; /* 'your' (client) IP address */
    	/* IP address of next server to use in bootstrap, returned in DHCPOFFER, DHCPACK by server */
    	uint32_t siaddr_nip;
    	uint32_t gateway_nip; /* relay agent IP address */
    	uint8_t chaddr[16];   /* link-layer client hardware address (MAC) */
    	uint8_t sname[64];    /* server host name (ASCIZ) */
    	uint8_t file[128];    /* boot file name (ASCIZ) */
    	uint32_t cookie;      /* fixed first four option bytes (99,130,83,99 dec) */
    	uint8_t options[DHCP_OPTIONS_BUFSIZE + CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS];
    } PACKED;
    #define DHCP_PKT_SNAME_LEN      64
    #define DHCP_PKT_FILE_LEN      128
    #define DHCP_PKT_SNAME_LEN_STR "64"
    #define DHCP_PKT_FILE_LEN_STR "128"
    
    struct ip_udp_dhcp_packet {
    	struct iphdr ip;
    	struct udphdr udp;
    	struct dhcp_packet data;
    } PACKED;
    
    struct udp_dhcp_packet {
    	struct udphdr udp;
    	struct dhcp_packet data;
    } PACKED;
    
    enum {
    	IP_UDP_DHCP_SIZE = sizeof(struct ip_udp_dhcp_packet) - CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS,
    	UDP_DHCP_SIZE    = sizeof(struct udp_dhcp_packet) - CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS,
    	DHCP_SIZE        = sizeof(struct dhcp_packet) - CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS,
    };

    Если в этом же файле мы спустимся чуть пониже, то увидим, что объявления функций работы с опциями так же находятся в одном месте:

    (файл networking/udhcp/common.h)

    unsigned FAST_FUNC udhcp_option_idx(const char *name);
    
    uint8_t *udhcp_get_option(struct dhcp_packet *packet, int code) FAST_FUNC;
    int udhcp_end_option(uint8_t *optionptr) FAST_FUNC;
    void udhcp_add_binary_option(struct dhcp_packet *packet, uint8_t *addopt) FAST_FUNC;
    void udhcp_add_simple_option(struct dhcp_packet *packet, uint8_t code, uint32_t data) FAST_FUNC;
    #if ENABLE_FEATURE_UDHCP_RFC3397
    char *dname_dec(const uint8_t *cstr, int clen, const char *pre) FAST_FUNC;
    uint8_t *dname_enc(const uint8_t *cstr, int clen, const char *src, int *retlen) FAST_FUNC;
    #endif
    struct option_set *udhcp_find_option(struct option_set *opt_list, uint8_t code) FAST_FUNC;

  • Не убирай безвозвратно неиспользуемый по умолчанию функционал

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

    Если бы они были раскомментированы — то это были бы макросы, которые не всплывают нигде в коде. Человек, который спросил бы «а зачем все эти опции?» искал бы ответ очень долго. И не нашел бы.

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

    (файл networking/udhcp/common.h)

    #define DHCP_PADDING            0x00
    #define DHCP_SUBNET             0x01
    //#define DHCP_TIME_OFFSET      0x02 /* (localtime - UTC_time) in seconds. signed */
    //#define DHCP_ROUTER           0x03
    //#define DHCP_TIME_SERVER      0x04 /* RFC 868 time server (32-bit, 0 = 1.1.1900) */
    //#define DHCP_NAME_SERVER      0x05 /* IEN 116 _really_ ancient kind of NS */
    //#define DHCP_DNS_SERVER       0x06
    //#define DHCP_LOG_SERVER       0x07 /* port 704 UDP log (not syslog)
    //#define DHCP_COOKIE_SERVER    0x08 /* "quote of the day" server */
    //#define DHCP_LPR_SERVER       0x09
    #define DHCP_HOST_NAME          0x0c /* either client informs server or server gives name to client */
    //#define DHCP_BOOT_SIZE        0x0d
    //#define DHCP_DOMAIN_NAME      0x0f /* server gives domain suffix */
    //#define DHCP_SWAP_SERVER      0x10
    //#define DHCP_ROOT_PATH        0x11
    //#define DHCP_IP_TTL           0x17
    //#define DHCP_MTU              0x1a
    //#define DHCP_BROADCAST        0x1c
    //#define DHCP_ROUTES           0x21
    //#define DHCP_NIS_DOMAIN       0x28
    //#define DHCP_NIS_SERVER       0x29
    //#define DHCP_NTP_SERVER       0x2a
    //#define DHCP_WINS_SERVER      0x2c
    #define DHCP_REQUESTED_IP       0x32 /* sent by client if specific IP is wanted */
    #define DHCP_LEASE_TIME         0x33
    #define DHCP_OPTION_OVERLOAD    0x34
    #define DHCP_MESSAGE_TYPE       0x35
    #define DHCP_SERVER_ID          0x36 /* by default server's IP */
    #define DHCP_PARAM_REQ          0x37 /* list of options client wants */
    //#define DHCP_ERR_MESSAGE      0x38 /* error message when sending NAK etc */
    #define DHCP_MAX_SIZE           0x39
    #define DHCP_VENDOR             0x3c /* client's vendor (a string) */
    #define DHCP_CLIENT_ID          0x3d /* by default client's MAC addr, but may be arbitrarily long */
    //#define DHCP_TFTP_SERVER_NAME 0x42 /* same as 'sname' field */
    //#define DHCP_BOOT_FILE        0x43 /* same as 'file' field */
    //#define DHCP_USER_CLASS       0x4d /* RFC 3004. set of LASCII strings. "I am a printer" etc */
    #define DHCP_FQDN               0x51 /* client asks to update DNS to map its FQDN to its new IP */
    //#define DHCP_DOMAIN_SEARCH    0x77 /* RFC 3397. set of ASCIZ string, DNS-style compressed */
    //#define DHCP_SIP_SERVERS      0x78 /* RFC 3361. flag byte, then: 0: domain names, 1: IP addrs */
    //#define DHCP_STATIC_ROUTES    0x79 /* RFC 3442. (mask,ip,router) tuples */
    #define DHCP_VLAN_ID            0x84 /* 802.1P VLAN ID */
    #define DHCP_VLAN_PRIORITY      0x85 /* 802.1Q VLAN priority */
    //#define DHCP_MS_STATIC_ROUTES 0xf9 /* Microsoft's pre-RFC 3442 code for 0x79? */
    //#define DHCP_WPAD             0xfc /* MSIE's Web Proxy Autodiscovery Protocol */
    #define DHCP_END                0xff

  • Инкапсулируй подобные функции в зависимости от предназначения.

    Довольно сложный для восприятия аспект, требующий пояснения. В двух словах: если у вас есть функция, которая внутри проекта вызывается с n комбинациями различных параметров (где n — небольшое число), причем каждая комбинация вызывается по нескольку раз, имеет смысл сделать для каждой комбинации отдельную функцию, вызывающую внутри себя целевую функцию, но уже с нужными параметрами. Например. У нас есть функция, отправляющая пакеты на определенный порт и адрес:

    (файл networking/udhcp/packet.c)

    /* Construct a ip/udp header for a packet, send packet */
    int FAST_FUNC udhcp_send_raw_packet(struct dhcp_packet *dhcp_pkt,
    		uint32_t source_nip, int source_port,
    		uint32_t dest_nip, int dest_port, const uint8_t *dest_arp,
    		int ifindex)
    {
    	struct sockaddr_ll dest_sll;
    	struct ip_udp_dhcp_packet packet;
    	unsigned padding;
    	int fd;
    	int result = -1;
    	const char *msg;
    
    	fd = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP));
    	if (fd < 0) {
    		msg = "socket(%s)";
    		goto ret_msg;
    	}
    
    	memset(&dest_sll, 0, sizeof(dest_sll));
    	memset(&packet, 0, offsetof(struct ip_udp_dhcp_packet, data));
    	packet.data = *dhcp_pkt; /* struct copy */
    
    	dest_sll.sll_family = AF_PACKET;
    	dest_sll.sll_protocol = htons(ETH_P_IP);
    	dest_sll.sll_ifindex = ifindex;
    	dest_sll.sll_halen = 6;
    	memcpy(dest_sll.sll_addr, dest_arp, 6);
    
    	if (bind(fd, (struct sockaddr *)&dest_sll, sizeof(dest_sll)) < 0) {
    		msg = "bind(%s)";
    		goto ret_close;
    	}
    
    	/* We were sending full-sized DHCP packets (zero padded),
    	 * but some badly configured servers were seen dropping them.
    	 * Apparently they drop all DHCP packets >576 *ethernet* octets big,
    	 * whereas they may only drop packets >576 *IP* octets big
    	 * (which for typical Ethernet II means 590 octets: 6+6+2 + 576).
    	 *
    	 * In order to work with those buggy servers,
    	 * we truncate packets after end option byte.
    	 */
    	padding = DHCP_OPTIONS_BUFSIZE - 1 - udhcp_end_option(packet.data.options);
    
    	packet.ip.protocol = IPPROTO_UDP;
    	packet.ip.saddr = source_nip;
    	packet.ip.daddr = dest_nip;
    	packet.udp.source = htons(source_port);
    	packet.udp.dest = htons(dest_port);
    	/* size, excluding IP header: */
    	packet.udp.len = htons(UDP_DHCP_SIZE - padding);
    	/* for UDP checksumming, ip.len is set to UDP packet len */
    	packet.ip.tot_len = packet.udp.len;
    	packet.udp.check = inet_cksum((uint16_t *)&packet,
    			IP_UDP_DHCP_SIZE - padding);
    	/* but for sending, it is set to IP packet len */
    	packet.ip.tot_len = htons(IP_UDP_DHCP_SIZE - padding);
    	packet.ip.ihl = sizeof(packet.ip) >> 2;
    	packet.ip.version = IPVERSION;
    	packet.ip.ttl = IPDEFTTL;
    	packet.ip.check = inet_cksum((uint16_t *)&packet.ip, sizeof(packet.ip));
    
    	udhcp_dump_packet(dhcp_pkt);
    	result = sendto(fd, &packet, IP_UDP_DHCP_SIZE - padding, /*flags:*/ 0,
    			(struct sockaddr *) &dest_sll, sizeof(dest_sll));
    	msg = "sendto";
     ret_close:
    	close(fd);
    	if (result < 0) {
     ret_msg:
    		bb_perror_msg(msg, "PACKET");
    	}
    	return result;
    }
    

    Но dhcp не всегда нуждается в отправке пакета на один IP адрес. В основном используется широковещательная (BROADCAST) рассылка.

    Но широковещательная отправка пакета — всего лишь отправка пакета по адресу, зарезервированному под broadcast. Собственно, для того, чтоб отправить широковещательный запрос, достаточно использовать описанную выше функцию, но в качестве адреса указать тот, что зарезервирован про бродкаст. Отсюда получаем функцию:

    (файл networking/udhcp/dhcpc.c)

    static int raw_bcast_from_client_config_ifindex(struct dhcp_packet *packet)
    {
    	return udhcp_send_raw_packet(packet,
    		/*src*/ INADDR_ANY, CLIENT_PORT,
    		/*dst*/ INADDR_BROADCAST, SERVER_PORT, MAC_BCAST_ADDR,
    		client_config.ifindex);
    }
    

    Профит этого в том, что везде, где мы будем встречать эту функцию, можно будет по названию понять, что она делает. Если бы мы использовали функцию udhcp_send_raw_packet, то нам бы осталось только гадать по параметрам.

Заключение




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

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

Пиши на Си как джентльмен.

Удачи, Хабр!
Поделиться с друзьями
-->

Комментарии (169)


  1. AlexMal
    06.04.2017 09:24

    Спасибо за статью.У меня есть вопрос, по поводу вендоринга в C и в C++, какая лучше всего практика по использованию и подключению сторонних библиотек и контроля их версий? И смотрели ли Вы в сторону таких молодых языков как Rust или Go?


    1. Indever2
      06.04.2017 10:27
      +1

      В крупных проектах (во всяком случае, в тех, в которых я участвовал) все необходимые библиотеки (включая libc-библиотеки) содержатся в исходниках и компилируются при сборке.
      То есть, в проекте присутствует только одна версия библиотеки. Хочешь другую — нужно озаботиться этим и установить её в проекте.

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

      А вообще есть clib, но я с ней не работал, ничего сказать о ней не могу:)
      Ну или, вот это менее популярное решение.

      С Rust я пока еще не знаком, с Go недавно начал возиться. Пока все очень нравится — система импорта пакетов после сей не перестает меня радовать :)


      1. AlexMal
        06.04.2017 10:40
        +4

        Да, но Go пока позволяет использовать только последнюю версию библиотеки.
        А вот как раз Cargo в Rust позволяет выбирать любую версию.
        Так что, советую ознакомиться, если будет время.
        Мне, например, очень нравится подход, который они используют.


        1. Indever2
          06.04.2017 14:13
          +1

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

          Rust недавно расхваливал один хороший знакомый, так что рано или поздно я к нему приду, думаю.
          А Cargo — уже как получится. Пока вообще не представляю, что это за язык:)


          1. ozkriff
            06.04.2017 14:23
            +1

            Rust недавно расхваливал один хороший знакомый, так что рано или поздно я к нему приду, думаю.
            А Cargo — уже как получится. Пока вообще не представляю, что это за язык:)

            Если что, cargo это пакетный менеджер раста.


            1. Indever2
              06.04.2017 15:58
              +1

              Спасибо за поправку, не знал :)


          1. AlexMal
            06.04.2017 14:23
            +1

            Cargo — это не язык, это утилита для Rust — пакетный менеджер, который отвечает за структуру проекта, сборку и за библиотеки.


            1. Indever2
              06.04.2017 15:58

              Ахах, не знал.
              Спасибо за просвещение :)


        1. mirrr
          07.04.2017 09:46

          Вендоринг в go позволяет заморозить библиотеку на нужном коммите (если только там не разветвленная цепь зависимостей, которые вытянуть проблематично. Еще есть мой любимый gopkg.in (от создателей mgo), позволяющий использовать даже несколько версий одного пакета в одном приложении. Но тут проблема доверия и оф.поддержки для многих может стать решающим фактором.


    1. yarric
      06.04.2017 21:06
      -1

      Область применения Rust или Go несколько отличается от системного программирования, особенно Go.


      1. grossws
        06.04.2017 21:54
        +2

        Интересно, почему область применения rust — не системное ПО? Он в этой нише, в первую очередь, и интересен.


        1. yarric
          07.04.2017 20:25
          -2

          По скорости Rust не дотягивает до C, насколько я могу судить.


          1. grossws
            07.04.2017 20:49

            В каких кейсах, при каких структурах данных, при каких используемых абстракциях?


            http://benchmarksgame.alioth.debian.org/, если что, не аргумент, там много откровенно кривых бенчмарков.


            1. Psychopompe
              08.04.2017 05:53

              А есть альтернативные бенчмарки?


    1. eugenebabichenko
      08.04.2017 13:46
      +1

      Если в проекте используется CMake, то есть ещё один вариант: таки указать в CMakeLists.txt репозитории и конкретные версии, которые нужно тянуть. К примеру, в мануалах ко многим библиотекам от Google рекомендуется подключать их именно так.


  1. kloppspb
    06.04.2017 09:35
    +1

    Если уж упоминается restrict, то можно было бы вспомнить и про inline-функции, константы (в разделе про макросы), про именованую инициализацию структур и расширеную массивов (особенно в последнем разделе) etc.
    А вот register на этом фоне смотрится уже какой-то тенью прошлого :)

    За valgrind отдельное спасибо, почему-то до сих пор есть люди, которые про него не знают.


    1. Indever2
      06.04.2017 10:37

      Да, возможно. Выбрал довольно абстрактную тему для статьи — сложно охватить все и разом. Правда, inline я вижу постоянно, а те же volatile и restrict — довольно редко, даже в тех местах, где их использование напрашивается.
      Скажем так, узнать про inline проще, чем про restrict и volatile — вот я и решил это упомянуть :)

      А register — да, потихоньку выходит из использования. Тем не менее, иногда — полезная вещь.


      1. Gumanoid
        06.04.2017 16:05

        register вообще ни на что не влияет в современных компиляторах при уровне оптимизации -O1 и выше. (современные — это где-то с конца 1980х). Но на -O0 может влиять. Вот например обсуждение в рассылке GCC: https://gcc.gnu.org/ml/gcc/2010-05/msg00098.html


        1. LynXzp
          08.04.2017 23:50
          +2

          Когда писал программы на streetinterview (сейчас hackerrank.com) моя программа не вкладывалась по времени буквально на процент в отведенный лимит. Я исключительно с помощью register (скорее методом итеративного поиска, чем логически, потому что были и очень не очевидные последствия) подобрал так чтобы программа на моем AMD работала процентов на 5 быстрее. Отправил программу на streetinterview и увидел падание производительности на все 10% на их XEON. Для убедительности запустил пару раз и с тех пор register не использую, чтобы не снился.


    1. billyevans
      08.04.2017 08:26
      +1

      Да, только он только на однопоточный простейших приложений и работает. В более-менее сложном чем-то он тупо замедляет все раз в 100 примерно и ничего толком не работает. Мне больше нравится вариант использовать jemalloc с опцией дебага heap-а. Там можно и в рантайпе профили снимать, графы смотреть, что откуда пришло. Различные графики строить и тд. Ну и утечки тоже ищет. И оно почти не замедляет ничего(по крайней мере приложение приемлемо работает с ним), памяти только больше жрет, что ожидаемо.
      https://github.com/jemalloc/jemalloc/wiki/Use-Case%3A-Heap-Profiling


      1. kloppspb
        08.04.2017 20:45

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

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


        1. billyevans
          08.04.2017 21:32

          Можно без перезборки.
          Достаточно через LD_PRELOAD подгрузить so-ку, jemalloc-а собранного с поддержкой поиска утечек.
          Так через переменные окружения можно настроить, а настроек там тьма.


      1. 4144
        09.04.2017 23:36
        +1

        Как я понимаю jmalloc это нестандартный аллокатор и отладчик для хипа. А valgrind это проверка всего и сразу. Хотя часть можно отключить. Он проверяет переполнения, обращения по некорректным адресам, утечки и д.р.

        Еще есть более быстрый вариант. Это asan/ubsan. Он может проверять уже кое что, чего valgrind не может, но в тоже время чуть менее надежно.
        Для использования *san надо собирать приложение с нужными флагами. Работа замедляется в несколько раз, но получается почти c/c++ с проверкой корректности работы с памятью.


        1. kloppspb
          10.04.2017 00:30

          Он может проверять уже кое что, чего valgrind не может,

          Например?
          Для использования *san надо собирать приложение с нужными флагами
          В valgrind мне нравится как раз то, что для проверок не нужны никакие вмешательства в сборку и прочие дополнительные телодвижения. Но при этом он удобно интегрируется в IDE. Например, в том же Eclipse (да-да, я им вовсю пользуюсь) живёт в профайлерах:

          image

          С остальными ещё пока руки не дошли поразбираться, но интересно.


          1. 4144
            10.04.2017 00:56

            Например?


            Скажем у вас есть структура что-то вроде
            struct
            {
              int a[15];
              int b;
            };
            


            a[16] для valgrind скорее всего будет корректен, если не вставится паддинг после a. А ubsan или asan выведет ошибку.
            Или тоже самое, только уже разные структуры или массивы, но по указателю из первой структуры через переполнение индекса вы попадете, во вторую. Тут тоже asan/ubsan должен сработать.
            Пропустить может если там будут указатели а не массивы.

            Другие случаи не помню, но они тоже есть.


            1. kloppspb
              10.04.2017 01:39

              Ну, если в лоб, то gcc и сам отловит :) И если индекс задаётся переменной, значение которой компилятору известно. Но идея понятна, спасибо.

              P.S. А вот cppcheck не поймал, даже если индекс явно 16 влепить. Надо ещё PVS замучить, но позже.

              P.P.S. Кстати, немного не про это, но про С. Вроде бы мелочь, но приятно, чёрт возьми :)


              1. 4144
                10.04.2017 02:41

                да, конечно я имел в виду что индекс задается в рантайме. Скажем читается из файлы, или как-то вычисляется.


              1. JIghtuse
                10.04.2017 08:38

                P.S. А вот cppcheck не поймал, даже если индекс явно 16 влепить.

                Возможно баг, или информации анализатору не хватает. Вообще он такое ловит:


                (error) Array 's.a[15]' accessed at index 16, which is out of bounds.


                Насчёт sanitizers — отличные инструменты. Замедляют программу значительно меньше valgrind, а находят зачастую больше проблем, хотя бы за счёт большего объёма информации о программе. Пересобирать надо, да, но оно того стоит.


  1. Akon32
    06.04.2017 09:40
    +9

    Объявляйте переменные в начале функции.

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


    По возможности инициализируйте переменные при объявлении. Численные с помощью нуля, указатели — NULL.

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


    примеры (java)

    Не инициализируем сразу:


    int a;
    if(something){
        a = 3;
        b = 5;
        c = 9;
    }else{
        // тут забыли присвоить a=1;
        b = 2;
        c = 1;   
    }
    f(a,b); // не компилируется (a не инициализирована), сразу исправляем.

    Инициализируем сразу:


    int a=0;
    if(something){
        a = 3;
        b = 5;
        c = 9;
    }else{
        // тут забыли присвоить a=1;
        b = 2;
        c = 1;   
    }
    f(a,b); // компилируется (a инициализирована, но не тем, чем надо), ловим баги в рантайме.


    1. l4l
      06.04.2017 09:55

      Объявляйте переменные в начале функции.

      Как минимум это релевантно в Си, ибо не перечит стандарту ansi


      1. Akon32
        06.04.2017 10:32
        +3

        В ansi c, насколько я помню, "не в начале" объявить даже нельзя, в С99 — можно. Если уже есть компилятор С99, не всегда нужно следовать ansi c.


        1. l4l
          06.04.2017 12:12

          Более того, хватает компиляторов с поддержкой c11
          Но как правило последний стандарт не отражает стандарт который используется (требуется поддерживать)


    1. kloppspb
      06.04.2017 10:00
      +1

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

      Если функция не растягивается на хрелиард строк, то ничего вспоминать и не придётся. Одна из причин такого объявления в C++ или Java в том, что для не-POD-типов существуют конструкторы и деструкторы. И нет никакого смысла их дёргать (а они могут быть далеко на самыми лёгкими), если код пойдёт по ветке, в которой эти переменные просто не используются. В C такой проблемы нет (если не учитывать инициализацию). Привычка, скорее, тянется из ISO C :-)


    1. l4l
      06.04.2017 10:02

      И да, кстати


      Не уверен, но мне кажется, что в настоящее время ключевое слово register не влияет ни на что, по крайней мере на самых распространенных платформах.

      src без register

      int sqr(int a) {
      return a*a;
      }


      1. grossws
        06.04.2017 10:22
        +2

        Это с -O2?


        1. Akon32
          06.04.2017 10:28

          clang, icc компилируют одинаково; gcc начиная с -O1 — одинаково, а без оптимизаций — так, как на скринах.
          https://godbolt.org/g/F5IFpG


          1. grossws
            06.04.2017 10:43

            Не писал тэг "сарказм", думал и так поймут. Естественно, при включенной оптимизации будет что-то вида


            mov eax, edi
            imul eax, eax
            ret

            или функциональный аналог.


        1. l4l
          06.04.2017 10:29
          +1

          Нет (: пример с оптимизацией будет придумать посложнее


          1. kloppspb
            06.04.2017 12:08
            +2

            К тому же, в соответствии с ABI, указание register в параметрах функции вообще бессмысленно. А если брать «игрушечные» примеры, то и вовсе:

            static int sqr(int a) {
                return a*a;
            }
            
            int main(void) {
                return sqr(123);
            }

            превратится в:

            main:
            .LFB61:
            .cfi_startproc
            movl $15129, %eax
            ret


            Или это:

            int main(int argv, char **argc) {
                return sqr(argv);
            }

            станет

            main:
            .LFB61:
            .cfi_startproc
            movl %edi, %eax
            imull %edi, %eax
            ret


    1. Amelius0712
      06.04.2017 10:02

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


    1. Indever2
      06.04.2017 10:10
      -1

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

      int foo;
      /* ... спустя n строк в той же области видимости...*/
      int foo = VALUE_FOR_FOO;
      

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


      1. arcman
        08.04.2017 20:48

        Что-то я не припомню аналога прототипов функций, но для переменных. Это какое то дополнение в стандарте позволяющее два раза объявить переменную?


        1. kloppspb
          08.04.2017 21:01

          Скорее всего пропущено extern :)


          1. arcman
            13.04.2017 14:15

            не, нельзя так, автор сморозил просто.
            будет либо «error: non-extern declaration of 'foo' follows extern declaration»
            либо «error: 'extern' variable cannot have an initializer»


            1. kloppspb
              13.04.2017 15:10
              +1

              Почему же? Вот вполне валидный код:

              #include <stdio.h>
              
              extern int foo;
              /* ... спустя n строк в той же области видимости...*/
              int foo = 0xDEAFBEEF;
              
              int main(void) 
              {
                  return printf( "%08X\n", foo );
              }


    1. sshikov
      06.04.2017 13:59
      +1

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


      А это предполагает автоматически, что инициализация при объявлении, один раз и навсегда. Так что совет-то как раз годный. И для всех языков осмысленный. И именно в java есть смысл так делать почти всегда, добавив еще и final.


    1. sumanai
      06.04.2017 16:36

      чтобы при чтении не приходилось «вспоминать» типы увиденных переменных.

      IDE же подскажет типы при наведении.


      1. l4l
        06.04.2017 19:43

        Но нужно делать лишние действия с каждой такой переменной


  1. sim-dev
    06.04.2017 09:41
    +4

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

    Мне не нравится идея писать макросы-функции капсом. Я рассуждаю логически (логика и программирование — это же стороны одной монеты, не так ли?): макрос-функция скрывает в себе некое законченное действие, т.е. по смыслу неотличим от функции. Так зачем выделять внешним видом его из ряда функций? Только для того, чтобы знать, что это макрос? И что дает это знание? Рано или поздно может возникнуть ситуация, когда макрос на самом деле придется заменить на функцию, и тогда придется править тонны кода, меняя капс-идентификатор на некапс-идентификатор…
    С моей т.з. логично и правильно отличать идентификаторы переменных от констант, для чего и служит капс-выделение. Отличать функционально завешенный блок кода, решающий часто повторяющуюся или логически обособленную задачу, оформленный макросом или функцией смысла не вижу.

    Что касается других «рекомендаций», то увы и ах, при работе с встраиваемыми системами некоторые «правильные» принципы не работают: «красиво завершиться, напоследок сказав, что именно пошло не по плану» программа попросту не может… Например, популярный нынче квадрокоптер: в случае ошибки «красиво завершиться» — это значит, перед падением на асфальт сделать мертвую петлю? Или осколки разбросать в виде кода ошибки? Во многом количестве встраиваемых систем НЕТ ОС, которая могла бы воспользоваться кодами ошибок. И так или иначе приходится писать программы, умело игнорирующие ошибки, т.е. допускающие деление на ноль без видимых для пользователя последствий.
    Хотя сам принцип -да, разумен. Но не абсолютен.

    И, таки-да, автор сам себе противоречит, выделяя функции-обертки капсом…


    1. saluev
      06.04.2017 10:26
      +10

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

      Рано или поздно может возникнуть ситуация, когда макрос на самом деле придется заменить на функцию
      Не надо писать макросов, которые можно заменить на функции.


      1. sim-dev
        07.04.2017 07:52

        Функции с результатом void тоже нельзя передавать, как аргумент в другие функции, — и что?
        Любая попытка давать ОДНОЗНАЧНЫЕ рекомендации будет ошибочной.
        Функции, макросы — это все абстракции, призванные что-то от читающего скрыть, упростить, уменьшить количество анализируемых сущностей. С точки зрения человека их смысл именно в этом. Поэтому сущности с одинаковым смыслом (в человеческом понимании) не стоит явно разделять.
        Вот объясните мне логику создателей GCC, которые спокойно сделали два макроса _BV(x) и bit_is_set(a,b) — какой логикой они руководствовались, делая один заглавными, а второй нет?


        1. saluev
          07.04.2017 09:29
          +3

          Функции с результатом void можно передавать. Я имел в виду что-то вроде

          void do_things(void) {
              ...
          }
          
          void do_async(void (*what)(void)) {
              ...
          }
          
          ...
          
          do_async(do_things);
          

          Макросы (в современном языке) нужны только там, где функций недостаточно. Там, где нужно генерировать названия функций, например. Или как-то ещё расширять синтаксис языка.

          Логику создателей GCC я вам объяснить не смогу, я не принадлежу к их числу.


          1. sim-dev
            07.04.2017 09:33

            Обидно, что мои слова вы поняли буквально.
            «Передавать функцию» я, разумеется, имел ввиду «передавать результат функции», а не ее адрес.


            1. Sirikid
              11.04.2017 20:00

              Если не считать void типом, как это сделано в Java, все становится проще.


              1. kloppspb
                11.04.2017 20:23

                Вот чесслово, не понимаю: зачем в одном языке делать как в другом. Эдак можно договориться и до упрощённого клингонского, или того круче, «ку» и «кю» :-)


                1. Sirikid
                  11.04.2017 20:47

                  Потому что это допущение многое упрощает. В Java Language Specification написано


                  void это ключевое слово, которое указывает на то, что функция не возвращает никакого значения

                  Как минимум больше не возникает вопрос создания переменной с типом void и значением типа void (гипотетический результат void-функции) — нету такого типа.


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


                  1. kloppspb
                    11.04.2017 21:26

                    В других языках это понятно зачем. Но ответа на вопрос «зачем это в C» вы так и не дали.


                    1. Sirikid
                      11.04.2017 21:33

                      Упростить спецификацию языка.


                      1. kloppspb
                        11.04.2017 21:43

                        Что именно в ней кажется вам сложным?


                        1. Sirikid
                          11.04.2017 21:55

                          Есть тип void, но нету термов. Функции могут возвращать void, но переменных типа void не может быть. Вам не кажется что это не тип, а нечто иное?


                          1. kloppspb
                            11.04.2017 22:04
                            +1

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

                            Функции могут возвращать void
                            Нет, не могут. void-функция в C не возвращает ничего, никакого «возвращаемого значения» у неё нет, и сама мысль о его использовани не должна возникать, by design.

                            P.S. Не путать с void *


    1. Indever2
      06.04.2017 10:53

      Я не нес в своей статье посыл «Пишите только так и никак иначе» — каждый волен выбирать, какого стиля придерживаться. Те рекомендации по оформлению кода, что я привел — сборка наиболее часто используемых принципов как из проектов компании, где я работаю, так и из opensource проектов.

      У новичка, читающего данную статью, появится представление о том, как обычно делают более опытные ребята в самых разных проектах :)

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


    1. Indever2
      06.04.2017 10:56

      Да, кстати, капсом я обертки не выделяю
      о_О
      Я пишу их с заглавной буквы.
      Кто то использует _ перед именем обертки, кто-то — другие обозначения. Тут тоже есть свобода выбора :)
      О_о


    1. lpwaterhouse
      06.04.2017 21:19
      +2

      Если у вас есть сомнения: написать макрос или функцию — пишите функцию, не прогадаете. При соблюдении этого простого правила макрос никогда в жизни не выродится в функцию.


      1. sim-dev
        07.04.2017 07:42
        -5

        Увы, и Ваша рекомендация не панацея. Частенько приходится делать как переход от макроса к функции, так и наоборот. Это же не С++ с его шаблонами… Простой пример

        #define max(x,y) ((x)>(y))?(x):(y)
        
        В Си это не раз выручает при сравнении чисел любых типов, в то время как функций пришлось бы писать гораздо больше и с не такими лаконичными именами
        Или в «малых» встраиваемых системах: работу с портом ввода-вывода выгоднее делать макросом в силу большей скорости исполнения кода…


        1. alexeykuzmin0
          07.04.2017 10:42
          +3

          Ага, а потом кто-то пишет

          x = max(a++, b);
          

          И удивляется результатам. Ну или правило о том, что так нельзя делать, пишется в Code Style Guide, который в итоге раздувается.


          1. sim-dev
            07.04.2017 11:26

            Еще один пример того, что абсолютно правильных рекомендаций не бывает.


        1. LynXzp
          08.04.2017 22:58
          +2

          Или в «малых» встраиваемых системах: работу с портом ввода-вывода выгоднее делать макросом в силу большей скорости исполнения кода…
          Всегда пишу такие макросы функции со словом static, это позволяет компилятору самому принимать решение, как оптимальнее — заинлайнить как макрос или сделать функцией. Только что сравнил:
          #define CLR(a,b) ((a) &=~(1<<(b)))
          static void clr(volatile uint8_t *PORT,uint8_t bitN)
          	{	*PORT &= ~ (1<<bitN);	}
          Результат один к одному. (Правда при использовании функции приходится писать лишний амперсант при вызове функции. В данной статье от разработчиков микроконтроллеров конечно сказано что макрос работает быстрее… Но я объявил функцию static и компилятор имеет полное право ее заоптимизировать, что он и сделал. Убрав static — получаю гораздо большую программу.)

          А с учетом что в коде у меня (и не только, не нахожу статью на хабре), присутствуют далее такие короткие функции:
          static void Led_Off(void)
          	{	clr( &DDRC, 3 );
          		clr( &PORTC, 3 );	}

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

          P.S. В «малых» встраиваемых системах у меня все функции static (кроме main), сборка простая — в основной файл сначала все .h файлы инклудяются, потом все .c — все для того чтобы у компилятора были все возможности.


          1. kloppspb
            08.04.2017 23:12
            +2

            Убрав static — получаю гораздо большую программу.)

            Ну так это понятно. У меня тоже давно выработан автопилот: все функции внутри единицы компиляции объявляются как static, если внешнее связывание не требуется явно. И это не зависит от цели сборки.


    1. SamaRazor
      07.04.2017 03:37
      +2

      Зуд происходит неспроста. Человек который +- постоянно пишет на си, прекрасно знает, какое там раздолье на стрельбу по конечностям.

      #define cmp(x, y) (x - y)
      

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

      По поводу операционных систем: скоро линукс и его собратья будут в каждом чайнике стоять. И это прекрасно. В mbed есть тоже есть всякие RTOS. Причем ошибки все равно обрабатываются, нередко для этого заводят обработчики по systick.


  1. Indemsys
    06.04.2017 09:45
    +1

    Системщикам неплохо бы помнить, что C является основным языком при программировании встраиваемых систем и узлов IoT на малых микроконтроллерах.
    И там совсем другие подходы.
    Поэтому статьи по C надо начинать с дифференциации.
    Т.е. сразу предупредить, о программировании каких платформ идет речь и о каком организационном подходе к проектированию.

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



    1. Indever2
      06.04.2017 11:09

      Как я писал в комментариях выше, я не говорю, что нужно писать только так. Это — рекомендации, сделанные на основе самых распространенных практик оформления кода в самых разных проектах.

      А по поводу адаптации к рефакторингу… Это очень сложная тема.
      Когда я только начинал учить Си, меня часто тюкали по голове за то, что я пишу по-своему, игнорируя общепринятые вещи. Тогда же мне довелось участвовать в небольшом проекте, на последней стадии разработки которого одному опытному программисту поручили выполнить рефакторинг моего кода, ибо он не соответствовал кодстайлу проекта. А кодстайл проекта мы взяли такой же, как в ядре Linux.

      Однако, каждый волен сам выбирать свой кодстайл, главное чтоб он не противоречил соглашениям, принятым в проекте:)


    1. wigneddoom
      06.04.2017 22:11

      Разрешите поинтересоваться, это какие же подходы используются при разработке для малых микроконтроллеров, что указанные автором подходы не приенимы?


      1. LynXzp
        08.04.2017 23:13

        Не знаю что имел в виду автор, но встречал как «тяп ляп и впродакшн» так и «я художник, я так вижу» (ну с очень индивидуальным подходом в программировании). Такой код write only и стоит его почитать, но ровно с той целью с которой стоит посмотреть картины авангардизма.

        Хотя я такой подход не приветствую, но по своему опыту скажу что можно сдать код который «пора бы отрефакторить», но можно прицепить еще один костыль и будет работать.

        Правда бывают и требовательные заказчики, один у меня уже обновления в течении пяти лет заказывает, вот там в исходнике — красота, хоть на выставку.


  1. Holix
    06.04.2017 09:59

    Редко, когда я, почти, полностью согласен с содержанием статьи. Автору спасибо!


  1. AllexIn
    06.04.2017 10:02
    +1

    потихоньку был вытеснен из десктопа и энтерпрайза

    Да неужели?
    https://habrahabr.ru/company/hh/blog/318450/
    Да, у Java там почти в два раза отрыв по сравнению с С++, но что-то подсказывает, что это не десктоп с энтерпрайзом.
    Я вот сейчас пишу с ПК на котором одно приложение на C# и штук пять на Java. И это не что-то такое особенное, вполне себе мейнстримовая ОС. Остальное, С++ и C. Странно, да?


    1. yarric
      06.04.2017 21:13

      И как много современных десктопных приложений написано на чистом С?


      1. AllexIn
        06.04.2017 21:15

        С или С++?
        На С написано мало. Но вытеснил его отнюдь не Java и C#. А С++


        1. yarric
          06.04.2017 21:25
          +1

          Просто в статье и написано, что был вытеснен именно C. C++ от C уже совсем далеко ушел, их даже не стоит рядом упоминать.


          1. AllexIn
            06.04.2017 21:32
            +1

            В статье:
            «был вытеснен из десктопа и энтерпрайза такими высокоуровневыми гигантами как Java и C#»
            А С++ он не был вытеснен. Он был достаточно плавно заменен, причем часто с сохранением кодовой базы.


  1. Sirikid
    06.04.2017 10:10
    +7

    Дополнение к предыдущему пункту — файлы сборки круто размещать в каждой из таких отдельных директорий, и просто вызывать их из файла сборки в корневой директории. В таком случае Makefile в корневой директории будет выглядеть примерно так:
    .PHONY clean build
    build:
    cd sound/ && make clean && make 
    cd graphics/ && make clean && make
    cd engine/ && make clean && make
    sound:
    cd sound/ && make clean && make
    graphics:
    cd graphics/ && make clean && make
    engine:
    cd engine/ && make clean && make
    clean:
    cd sound/ && make clean
    cd engine/ && make clean
    cd greaphics/ && make clean

    Отвратительно, пусть будет хотя бы так:


    .PHONY: build clean
    
    build:
        make -C sound
        make -C graphics
        make -C engine
    
    clean:
        make -C sound clean
        make -C graphics clean
        make -C engine clean
    


    1. Akon32
      06.04.2017 10:44
      +2

      Там ещё и ошибка была, вероятно. Если только в make текущий каталог не сбрасывается при переходе к следующей строке блока в Makefile, то make будет вызван в sound/, sound/graphics/, sound/graphics/engine/.


      1. Indever2
        06.04.2017 11:11

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


        1. mva
          11.04.2017 18:42

          Я бы, всё же, очень попросил исправить вас этот Makefile к виду с вызовами $(MAKE) вместо make. Иначе это ломает многопоточную сборку.


          А ещё лучше — совместить это с makefile'ом от Sirikid. :)


          1. mva
            11.04.2017 18:45

            поправка: может сломать в некоторых случаях


      1. aragaer
        07.04.2017 03:38

        В make каждая строка выполняется в отдельном сабшелле, поэтому все сбрасывается.

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

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


        1. Sirikid
          07.04.2017 05:24

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

          Покажете каким именно образом?


          1. aragaer
            07.04.2017 09:15
            +1

            С конкретно такими «подпроектами» сильно красиво не получается, поэтому через шелл циклы:

            SUBDIRS = sound graphics engine
            .PHONY: all build clean
            
            all: build
            
            build clean:
                for dir in $(SUBDIRS); do         make -C $$dir $@;     done
            


            Или с макросами
            SUBDIRS=sound graphics engine
            
            define SUB
            	# DO NOT REMOVE THIS LINE
            	$(MAKE) -C $(1) $(2)
            endef
            
            .PHONY: all build clean
            
            all: build
            
            build clean:
            	$(foreach dir,$(SUBDIRS),$(call SUB, $(dir),$@))
            


            Вобщем все печально, если не поступить примерно так:
            SUBDIRS=sound graphics engine
            SOURCES=$(foreach dir, $(SUBDIRS), $(wildcard $(dir)/*.c))
            OBJECTS=$(subst .c,.o,$(SOURCES))
            
            .PHONY: all clean
            
            all: program
            
            program: $(OBJECTS)
            	$(LD) -o $@ $^
            
            clean:
            	-rm $(OBJECTS)
            


            Мы не можем использовать имена каталогов в качестве таргетов (то есть можем, но будем при этом страдать), потому что они с одной стороны существуют, а с другой стороны ни от чего не зависят. Можно было бы для каждого каталога определить связанную с ним статическую либу (sound -> libsound.a), которую бы и собирал соответствующий sub-make, но это не спасет от необходимости делать clean все равно по каталогам (find -name '*.o;' это тоже не самый правильный путь).

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


            1. Sirikid
              11.04.2017 20:12

              А зачем нам в таком случае make? Лучше уже шелл-скрипты нормальные сделать.


              1. aragaer
                11.04.2017 20:25
                +1

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

                Я бы очень хотел, чтобы описанная выше архитектура была преувеличением. Но нет, это суровая реальность на моем текущем месте работы.


    1. nolane
      07.04.2017 05:57
      +2

      1. JIghtuse
        07.04.2017 06:44

        Да-да, как раз хотел кинуть это. Пишут Makefile рекурсивные и потом жалуются на скорость работы make.


        1. billyevans
          08.04.2017 08:19

          Тут не скорость работы make-а, а проблема отслеживания зависимостей. Тк make тупо не знает про вложенные и не может правильно понять, что нужно пересобрать.


          1. aragaer
            08.04.2017 09:22

            Знает, если ему объяснить. В приведенном примере make ничего не объяснили о зависимостях.


            1. billyevans
              08.04.2017 09:47

              А каким образом ему можно сообщить? Простой пример.

              mybin: bin.c libsound.a:
                  cc $(^) -o $(@)
              
              libsound/libsound.a:
                  make -C libsound
              


              Поменяли файл локальный bin.c, не из libsound/. В этом примере make попытается пересобрать libsound.a, тк на этом уровне он не знает, что libsound.a не зависит от bin.c.
              Если сделать это без вложенного make в libsound можно сделать полный граф зависимостей и тогда make-у будет понятно, что libsound.a не зависит от bin.c и его трогать не нужно.
              Как можно тут указать правильно все от чего зависит libsound.a не дублирую Makefile из libsound?


              1. aragaer
                08.04.2017 14:00

                Он зайдет в libsound, запустит там make, увидит, что делать ничего не надо, выйдет. Да, уйти в sub-make придется, но и все.

                Есть вариант, который используется в некоторых проектах, поставить libsound.a в зависимость libsound/stamp, на который делается touch в конце сборки libsound.a, а при любой правке в каталоге libsound придется делать touch на этот stamp. Такое имеет смысл, если предполагается, что содержимое libsound будет меняться редко и вообще это сторонний код, которому мы верим.


                1. billyevans
                  08.04.2017 19:51

                  Если представить проект, где, например, с десяток таких библиотек и они по-разном зависят от каких то хедеров. Если Makefile общий с include вместо вызова вложенного make, тогда есть полный граф зависимостей и будет правильно работать -j например. Сразу будет понятно, что можно распараллелить, а что нет. В случае с раздельными же, нужно либо както этот -j передавать ниже, либо всегда с ним запускать. Так же, подобные внешние вызовы будут своего рода барьерами, мешая make-у работать параллельно иногда и ожидая вложенного завершения. Короче в статье, которую привели выше все хорошо описано и почему не нужно так делать, если хочется нормальной сборки. Минусов у единого Makefile, если это один проект, я не вижу. Некой модульности можно достичить include-ами, если хочется собрать только одну либо, то можно написать make libsound/libsound.a
                  Я просто сталкивался в относительно большом проекте(2М строк), как раз с реккурсивным make и неправильным отслеживанием зависимостей отчасти из-за этого. Сборка была очень медленной(С++ с шаблонами, бустами всякими и подобным шлаком), плохо параллелилась и делала много лишнего на каждый чих, когда не нужно.


                  1. aragaer
                    08.04.2017 20:28

                    -j передается в sub-make автоматически (в отличие от некоторых других флагов), если внешний make понял, что это sub-make. Для этого стоит либо использовать $(MAKE) вместо make, либо (надежнее) ставить + перед соответствующей строкой. На самом деле в этом случае sub-make даже идет параллельно с «внешним», джобы общие и вообще все шикарно работает.

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


                    1. nolane
                      09.04.2017 23:14

                      -j передается в sub-make автоматически

                      Польза от этого сомнительная. Получается общее количество потоков, используемое всеми запущенными makeами, будет отличаться от указанного. Это может ощутимо сказаться на времени сборки и используемой памяти.


                      1. aragaer
                        09.04.2017 23:24
                        +1

                        Как раз наоборот.

                        The ‘-j’ option is a special case (see Parallel Execution). If you set it to some numeric value ‘N’ and your operating system supports it (most any UNIX system will; others typically won’t), the parent make and all the sub-makes will communicate to ensure that there are only ‘N’ jobs running at the same time between them all. Note that any job that is marked recursive (see Instead of Executing Recipes) doesn’t count against the total jobs (otherwise we could get ‘N’ sub-makes running and have no slots left over for any real work!)
                        тыц


  1. Jef239
    06.04.2017 11:42
    +3

    char c[IP_UDP_DHCP_SIZE == 576 ? 1 : -1];
    

    Лично у меня, когда я это увидел, повис вопрос: «Чеееееееее?».

    Это абсолютно необходимая проверка для компиляции на разные архитектуры и разными компиляторами. Дело в том, что отключение выравнивания в разных компиляторах выполняется по-разному. Опять-таки архитектуры бывают разные, вплоть до 32битных байтов.

    Второй момент — почему так странно, а не просто assert. Дело в том, что обычный assert отрабатывает во время исполнения, а такой — во время компиляции. Сейчас обычно делают макрос CCASSERT, но тут, похоже, код древний, и на общепринятый макрос просто не перешли.

    Если интересно, то вот код макроса:
    #define _x_CCASERT_LINE_CAT(predicate, line) typedef char constraint_violated_on_line_##line[2*((predicate) != 0)-1];
    #define CCASSERT(predicate) _x_CCASERT_LINE_CAT(predicate, __LINE__)

    // Usage: CCASSERT(1) to pass; CCASSERT(0) to fail
    /*
    typedef struct {
    long x;
    long y;
    }foo ;
    CCASSERT(sizeof(foo) < 10) // will not complain
    */


    1. kloppspb
      06.04.2017 12:12
      +2

      Не на все платформы есть компиляторы, понимающие более новые стандарты Си.

      C99 уже 18 лет… Допускаю, что есть такие компиляторы, но примеры можно?


      1. Jef239
        06.04.2017 12:24
        +1

        MS-DOS, МСВС 3.0 (там gcc 2.95.4 и обновлять нельзя), думаю что PDP-11, VAX-11 и так далее… Плюс вагон военных машинок с нестандартной архитектурой…


        1. kloppspb
          06.04.2017 12:33

          Под МСВС собирал последний раз году в 2005, неужели они с тех пор так и заморожены? Вот это стабильность :)

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


          1. Jef239
            06.04.2017 12:38

            Суть в том, что в МСВС сертифицируется конкретная сборка под конкретную машину. Поставить более новую версию — это заново потратить год (и пару миллионов) на сертификацию. Так что если сертифицирована 3.0 — себе дороже затевать сертификацию 5.0.


            1. kloppspb
              06.04.2017 12:45

              Ну да, у нас для сертификации программы считали MD5 исходников/утилит/мейкфайлов etc, потом контрольные суммы того, что получилось после сборки. Плюс-минус байт уже не катит. Но когда это было и какое тогда было железо — с тех пор же многое изменилось. Неужели у нх не возникло желание использовать ОС на чём-то посвежей?


              1. Jef239
                06.04.2017 15:20
                +1

                Новые версии МСВС есть. Но кто будет сертифицировать новую версию на то железо, на которое уже сертифицирована старая? А сертифицируется именно бинарная сборка, если пересобрали из сорцов -уже сертификации нет. Принесли любой бинарнкик — опять сертификация слетела. Провели компиляцию не на МСВС — опять сертификации нет.

                Из рассказов коллег:
                — И предоставьте исходный код в распечатанном виде.
                — Там 600 тысяч строк, будет 15 тысяч листов, вес листа 80 грамм — итого 1200кг. Грузовик дадите?
                — Ладно, давайте на CD.

                :-)

                Ну в общем, если сертификация важнее всего, то остальным можно пожертвовать.


                1. alexeykuzmin0
                  06.04.2017 15:26
                  +2

                  Вообще-то, лист весит около 5 граммов, получается всего 75 кг.


                  1. Jef239
                    06.04.2017 16:00

                    Вы правы. Значит там по размеру получался грузовик


                    1. alexeykuzmin0
                      06.04.2017 16:12

                      Если в одну стопку сложить, то при стандартной плотности (80 г — толщина 0.1 мм) получится около 1.5 метров — тоже никак не гузовик. Может, у вас листов было в 10 раз больше?


                      1. Jef239
                        06.04.2017 16:36

                        Не, 1.5 метра — это до распечатки. Далее до такой толщины обратно в условиях офиса не сжать. Прессов-то нет…

                        Но даже 5 метров — в багажник влезет струдом. Газель нужна.


                    1. Akon32
                      07.04.2017 10:06

                      По размеру это 6 коробок размером порядка 0.3х0.3х0.3 м (если спрессовать так плотно, как A4 обычно лежит в пачках). То есть, 30 пачек A4 по 500 листов.


                1. kloppspb
                  06.04.2017 15:28
                  +1

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

                  А с тех пор и железо другое, и софтины сильно меняются, не sh ведь :) Но хозяин — барин, конечно.


      1. l4l
        06.04.2017 12:30

        tms320, snes (стандарт не нашел, но выглядит старым), v810 и даже x86


    1. Indever2
      06.04.2017 16:04

      Посмотрите вот этот мой комментарий, и комментарий выше.
      Понял ошибку, исправился :)


      1. Jef239
        06.04.2017 16:44

        Ну у нас Linux, FreeBSD, MS-DOS, Windows, QNX. Из компиляторов — gcc, lcc, clang, BC++, BCB, VC++… В итоге ровно к такому же решению и пришли. И именно потому, что не раз налетали.


  1. Jef239
    06.04.2017 11:56
    +2

    В использовании оберток есть небольшой минус, который, если захотеть, можно решить костылем. А что это за минус — можете предположить в комментариях :)

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

    А «небольшой» минус — это то, что как закроются сокеты — зависит от библиотеки. Если успели сделать connect по TCP/IP — сокет скорее всего закроется жестко и вторая сторона не будет в курсе, что мы уже отвалились. Так что лучше использовать atexit.


    1. Indever2
      06.04.2017 16:05

      Да, все абсолютно верно.
      Пометил себе, как появится заряд — с меня плюс в карму :)


  1. Amomum
    06.04.2017 12:52
    +4

    Храните заголовочники в директории include.

    А не могли бы вы объяснить, какой смысл делать отдельную директорию include? Я в этом вижу только минусы:
    • нужно прописывать еще один путь в настройках проекта
    • дерево проекта увеличивается вдвое, ведь внутри папки include приходится повторять всю структуру папок с файлами .c
    • в файле xx.c нельзя просто написать include «xx.h», приходится писать полный путь до него — #include «yyy/zzz/xx.h»
    • чтобы скопировать какой-то «модуль», вам приходится копировать два файла из разных мест

    Гораздо удобнее, на мой взгляд, группировать файлы в папке по принципу «модулей». Допустим, модуль Common — это отдельная папка, в ней common.c и common.h. Этот модуль приобретает «позиционную независимость», т.е. когда вы его копируете в другой проект, вам не надо переписывать инклуд в файле common.c. Ну и все минусы, описанные выше, пропадают.


    1. Indever2
      06.04.2017 13:41
      +1

      Возможно, я выбрал не очень удачный пример в статье. Да, группировка — очень хорошая практика. И она прекрасно дополняет метод с include'ом.

      Я уже писал — ко всему надо подходить с умом. Заголовки общего пользования, библиотечные заголовки, которые должны быть доступны во всем проекте. include — для них самое подходящее место.

      Возьмем тот же busybox. В исходниках все группируется именно так, как вы сказали. Каждая утилита в отдельной папке. И .c, и локальные заголовки. Но тем не менее, директория include там тоже есть, и все библиотечные хэдеры живут там:)

      Самый популярный файл во всем busybox — обитающий там заголовочник с интригующим названием «Libbb.h»

      По поводу полного пути — тут вы не совсем правы. Когда мы указываем директорию include, то поиск заголовочников при линковке будет происходить в ней. Так что можно обойтись указанием имени файла в кавычках :)


      1. Amomum
        06.04.2017 14:09

        По поводу полного пути — тут вы не совсем правы

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


    1. Videoman
      06.04.2017 23:51

      В папку include нужно класть только внешние заголовки библиотеки. Все внутренние заголовки удобнее держать рядом с компилируемыми файлами (.c), тут вы правы. Делается это для того, чтобы избежать роста сложности в настройках проектов использующих ваш модуль. Представьте, что в ваших внешних includa-x есть ссылки на include-ы сторонних библиотек, которые вы используете. В таком случае, пользователь вашей библиотеки просто не сможет использовать ее без настойки правильных путей к внешним библиотекам.
      Например, если есть module1 использующий module2 который, в свою очередь использует module3, а во внешних include-ах всех модулей присутствуют включения друг-друга, то module1 не получится использовать без include-ов модулей 2 и 3, хотя на самом деле достаточно только .lib файлов.


  1. domix32
    06.04.2017 13:03

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


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


    Не совсем понятно почему возникает необходимость использовать volatile переменные как в примере. Как минимум это усложняет код и добавляет магии в процесс разработки. Можно больше примеров когда такая магия оправдана?


    1. kloppspb
      06.04.2017 13:18

      Не совсем понятно почему возникает необходимость использовать volatile переменные как в примере.


      Там явно написано почему, в первом же предложении. Во втором — более развёрнуто.


    1. Indever2
      06.04.2017 13:25

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

      По поводу заголовочников стоит написать отдельную статью. В идеале, все заголовочники должны быть оформлены так, чтобы из них можно было понять как можно больше интерфейсов взаимодействия внутри программы.
      + надо понимать, что ничего не стоит делать бездумно. В том же busybox заголовочники бывают 2х типов: общего пользования и локальные. Общего пользования лежат в include в корне, а локальные — в папках с утилитами (подпроектами).
      Очень правильный комментарий вот тут, группировка — очень хорошая практика. Однако в системе вынос в инклуд я встречаю гораздо чаще.

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

      Тогда любое изменение этих переменных, которое будет сделано из другого, будет неявным для компилятора. Отсутствие volatile в этом случае может положить весь механизм IPC в приложении.


      1. domix32
        06.04.2017 14:14

        Каюсь, про многопоточность не подумал.


        1. grossws
          06.04.2017 16:10

          И не только, может и адрес из mmio-диапазона, как вариант. Можно, конечно, сказать, что это частный случай многопоточности, но потоков на том уровне может и не быть.


      1. alexeykuzmin0
        06.04.2017 15:17
        +1

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

        В C++ использование volatile в многопоточной среде — это очень вредный совет! Он просто не имеет свойств, которые могут быть нужны в многопоточной среде — например, атомарности изменений. Подробное обсуждение есть на stackoverflow


        1. kloppspb
          06.04.2017 15:30
          +2

          могут читать люди, пишущие на разных языках

          В C++

          Настоящий фидошник Subject не читает?


          1. alexeykuzmin0
            06.04.2017 15:32
            +2

            Вам смешно, а начинающий разработчик прочитает совет, подумает «ну C++ — это почти тот же C», и будет бездумно применять volatile там, где не надо. Навстречался я уже с такими, так что всегда не лишним будет предупредить лишний раз.


        1. Videoman
          07.04.2017 00:17
          +1

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


          1. Gryphon88
            07.04.2017 01:06

            Ещё забавно, как сочетается volatile и переупорядочивание строк компилятором. Тут можно придумать что-то кроме вставки на асме или специфичных для процессора инструкций?


          1. alexeykuzmin0
            07.04.2017 10:48

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


            1. Videoman
              07.04.2017 11:24
              +1

              Да, извините. Я отвечал Indever2. Не пойму как так вышло.


              1. Indever2
                07.04.2017 12:10

                Я прочитал ваш комментарий.
                Да, я не прав. Многопоточное приложение тут не при чем, компилятор в состоянии отслеживать изменения переменных в потоках.
                А вот когда несколько процессов могут менять одну переменную — другое дело :)
                Когда речь идет о изменении переменной извне процесса, то тогда оно будет неявным для компилятора.


                1. Videoman
                  07.04.2017 12:22
                  +2

                  Да. Но если совсем точно, то компилятор ничего не знает про потоки, абсолютно. Не знаю как в C, а в C++ только-только стали появляться TLS в стандарте, а вся работа с многопоточностью реализована в библиотеке. В любом случае на месте обращения к разделяемой потоками переменной будет нетривиальная функция или intrinsinc и компилятор нечего не выкинет. С volatile все по старинке.


                1. LynXzp
                  09.04.2017 00:23
                  +1

                  del. Ниже комментарий более подробный: https://habrahabr.ru/post/325678/#comment_10158130


      1. a1ien_n3t
        06.04.2017 15:48
        +1

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

        reg = 0x10;
        if(reg == 1)
        {
        do_somthing;
        }
        

        и reg будет без volatile то копилятор вправе выкинуть этот код.
        Или например мы пишем вот так
        reg = 0x10;
        reg = 0x11;
        ..
        reg = 0x35;
        

        Без volatile опять компилятор выкинет предыдущие операции и оставит только последнюю.


  1. kloppspb
    06.04.2017 13:18

    del


  1. orgkhnargh
    06.04.2017 14:07
    -1

    … а слова в названиях отделяются нижним подчеркиванием

    А какие еще бывают подчеркивания?


    1. Indever2
      06.04.2017 14:09

      Никакие :)
      Отчего-то все называют этот символ именно так :)
      Изначально хотел написать «нижний дефис», но меня раскритиковали.


  1. OlegZH
    06.04.2017 14:10

    У меня вызывает внутреннее противление приведённая в статье реализация функции

    static int CheckModemConnection()
    
    На первый взгляд, всё выглядит понятным. На это и рассчитано.
    Возвращает 1, если параметры, связанные с модемным соединением изменились, и 0, если нет.
    Насколько я помню, это традиционно для Си возвращать именно такие значения, чтобы в месте вызова можно было бы реализовать обработку для случая произошедших изменений. Было бы логичнее иметь в качестве кода возврата bool, но это же Си! (NB: надо свериться с последним стандартом!)

    Далее, название функции: CheckModemConnection. А если придётся проверять ещё чего-нибудь, то надо будет писать отдельную функцию для вот этого самого чего-нибудь? Мне бы ужасно захотелось бы обобщить и иметь общую для всех функцию проверки. Даже, если в Си нет классов. (Всегда можно ввести.) К тому же, для того, чтобы иметь возможность проверять состояние, неплохо бы как-то формализовать сие понятие и сделать так, чтобы, например, пробегать по списку необходимых свойств в цикле, а не создавать длинное условие для оператора if.

    В конце-концов, было бы крайне любопытно (и эффективно?) иметь текстовую строку, описывающую текущее состояние объекта, каждый символ которой связан с некоторым элементом описания модемного соединения. Делая простой проход по этой строке, можно было бы сразу получать ответ на нужный вопрос. А, если само символьное представление делать более сложным (XML?), то можно было бы автоматизировать выдачу диагностических сообщений (например, соединяя друг с другом строки состояния для различных элементов в единую строку).


    1. kloppspb
      06.04.2017 14:35

      Было бы логичнее иметь в качестве кода возврата bool

      Для C, скорее, традиционней ERROR_SUCCESS (0 в общем случае) или код ошибки (!= 0), или что изменилось, etc.

      NB: надо свериться с последним стандартом!

      C99 — stdbool.h


    1. Indever2
      06.04.2017 14:51

      Если бы надо было проверить что-то еще — да, мы бы написали отдельную функцию.
      И она бы вызывалась вместе со всеми проверками. CheckModemConnection() — одна из сотен функций, которые вызываются при обработке пришедшей конфигурации. То есть все — и сеть, и voip, и iptv имеет подобные проверки.
      Нужно это, чтобы группировать параметры, и, когда надо добавить новый, знать, где находится проверка группы связанных параметров.

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


      1. OlegZH
        06.04.2017 15:13

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


        1. Indever2
          06.04.2017 17:16

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


    1. wigneddoom
      06.04.2017 21:33

      "Традиционно" для Си функции действия add, get, write_ и т. д. должны возвращать отрицательный код ошибки, 0 или положительное значение в случае успеха. А функции предиката аналог bool — 0 и 1.


      В данном примере CheckModemConnection() из названия должна возвращать 0 — всё ок или отрицательный код ошибки. Поэтому лучше её было бы назвать как-то так: isModemConnectionChanged(), тогда сразу бы было ясно, что она "Возвращает 1, если параметры, связанные с модемным соединением изменились, и 0, если нет.".


      Слово традиционно я намеренно заключил в кавычки, т.к. если говорить о традициях, то язык Си неразрывно связан с историей Unix. Поэтому приверженцам традиций лучше следовать Linux kernel code style — пункт 16, либо KNF.


  1. themiron
    06.04.2017 14:30
    +3

    Костыль, созданный с одной целью — вставить его, как палку, в колеса тому, кто будет расширять этот код.
    Вернее, не так.
    Это — костыль, который ясно вам скажет: не надо повторять мою реализацию, хочешь пакет больше — указатели, динамическая память и динамическое вычисление размера тебе в помощь!
    Это — пример того, как делать не стоит. Никогда. Иначе случится насилие. Рано или поздно.

    Вы не поняли, и делаете неправильные выводы.
    Это проверка, что компилятор корректно упаковал структуру с нужным размером.
    См. https://git.busybox.net/busybox/commit/?id=6884f665bd7bc101f56ff9047afaffbc06dc99e2


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

    Это бессмысленное усложнение, т.к. нужна структура/буфер под максимальный размер DHCP пакета. Динамика — оверкил в условиях, когда пакеты обрабатываются строго последовательно и максимальный размер пакета заранее известен.
    Если DHCP клиент не обозначил серверу максимальный размер (57 опция), то сервер не имеет права по RFC отвечать пакетами бОльшего размера. Однако, это только в идеальном мире, поэтому есть опция UDHCP_SLACK_FOR_BUGGY_SERVERS, в которой задается размер дополнительного места в буфере с максимумом 924, что дает возможность принимать пакеты до 1500 байт.
    См. https://git.busybox.net/busybox/commit/?id=72e76044cfda377486a5199a0d35d71edf669a42


    Такой небольшой размер — прямое противоречие RFC с описанной в нем 57й опцией.

    Никакого противоречия нет. Такой небольшой размер находится в полном соответствии с минимальным MTU, ниже которого быть не может. Соответственно, т.к DHCP протокол основан на UDP без подтверждения доставки, этот небольшой размер дает "гарантию", что DHCP пакеты не потеряются по пути из-за более меньшего MTU.
    Чтобы информировать сервер о максимально поддерживаемом размере, добавляется 57 опция, с размером — IP_UDP_DHCP_SIZE == 576, вне зависимости от дополнительного буфера UDHCP_SLACK_FOR_BUGGY_SERVERS.
    См. коммиты 2007 и 2010 года:
    https://git.busybox.net/busybox/commit/?id=35ff74676b54b1cae5a6324d2517568393fedbc8
    https://git.busybox.net/busybox/commit/?id=b3af65b95de883e9be403e065f57b867d8ea8d43


    Таким образом, чтобы "законно" получать пакеты больше стандартного размера, нужно
    1) увеличить размер UDHCP_SLACK_FOR_BUGGY_SERVERS до максимально возможного
    2) уведомлять сервер о реально поддерживаемом размере 57й опцией с учетом текущего MTU интерфейса.
    И тут, всё давно придумано
    https://github.com/wl500g/wl500g/commit/57fd93bd29399d6b08643bb79a3e41f330b6cd9a


    1. Indever2
      06.04.2017 14:56

      Спасибо за проделанную работу по поиску информации по теме.

      Да, вы абсолютно правы, я не правильно понял замысел и интерфейсы bb.
      Извиняюсь.
      Сейчас отредактирую статью.


  1. cosmrc
    06.04.2017 15:32
    +3

    Объявляйте переменные в начале функции.

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

    Даже Google думает не так, как написано:
    C++ allows you to declare variables anywhere in a function. We encourage you to declare them in as local a scope as possible, and as close to the first use as possible.



    1. alexeykuzmin0
      06.04.2017 15:35

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

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


      1. cosmrc
        06.04.2017 15:42
        +1

        Зачем может понадобиться находить все переменные функции за раз?


        1. alexeykuzmin0
          07.04.2017 10:52

          Для упрощения чтения кода человеком, только что перешедшим с Pascal, например.

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


    1. kloppspb
      06.04.2017 15:44
      -1

      Как минимум для совместимости с ANSI / ISO стандартами.

      Даже Google думает не так

      И о другом языке.

      не нужно искать тип переменной

      https://habrahabr.ru/post/325678/#comment_10157022


      1. cosmrc
        06.04.2017 15:50
        -1

        Если мы говорим о совместимости, то стоит добавить советы типа «использовать битовый сдвиг вместо умножения»


        1. kloppspb
          06.04.2017 15:52

          Для современных (и даже не очень) компиляторов совет абсолютно бесполезный — они сами разберутся как оптимизировать арифметику. А к совместимости он вообще не имеет никакого отношения.


    1. cosmrc
      06.04.2017 15:47
      +2

      Сюда же в правило обычно добавляються переменные в `for loop`-ах. От этого горит еще больше.


      1. kloppspb
        06.04.2017 15:54

        Это плюсовая привычка, многим сишникам просто непонятная :)


    1. yarric
      06.04.2017 21:22

      ИМХО такое объявление добавляет читабельности: сразу прочитав список переменных, уже примерно понимаешь, с чем имеешь дело и что делает код.


      1. LynXzp
        09.04.2017 00:32

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


  1. yarkov
    07.04.2017 05:59

    Отличное руководство. Как раз для таких начинающих как я. Спасибо большое.


  1. saterenko
    07.04.2017 10:46
    +1

    По поводу инициализации переменных не соглашусь. Если переменные не инициализировать, компилятор будет ругаться, если она была использована без инициализации. Несколько раз это серьёзно выручало при написании конечных автоматов на case-ах.

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

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


    1. Indever2
      07.04.2017 18:37

      Плюсик за nginx.
      Очень изящная вещь.


    1. LynXzp
      09.04.2017 00:35

      Да, я бы порекомендовал вместо инициализации переменных включить это (и не только) в сообщения об ошибках и реагировать на них!


  1. Zapped
    07.04.2017 17:32
    +2

    Если вы постоянно работаете с трекерами (вроде RedMine), то при внесении правок в код можно указать номер задачи, в рамках которой эти правки были внесены. Если у кого-то при просмотре кода возникнет вопрос а-ля «Зачем тут этот функционал?», ему не придется далеко ходить. В нашей компании еще пишут фамилию программиста, чтобы если что знать, к кому идти с расспросами.
    /* Muraviyov: #66770 */


    Плохой совет. Плохой он оттого, что комментарии никто не поддерживает.
    Мировой опыт (выраженный в книжках МакКоннелла, Р. Мартина и пр.), а равно и мой скромный, говорит, что комментарии редко бывают актуальными, тем более, такие.
    «Кто, когда и почему?» — на эти вопросы достоверно отвечает система контроля версий (должна отвечать… у вас-то какая?).


    1. Indever2
      07.04.2017 18:35
      -1

      Когда мы делаем git blame в каком-нибудь огромном файле, в который каммитили все, кому не лень, найти изменения по какой-то конкретной фигне может быть довольно долго.
      + даже зная, кто и каким каммитом добавил функционал, не всегда можно понять, в рамках какой задачи это было сделано. (не все и не везде указывают).
      Да и в любом случае, просто вбить заранее указанный номер задачи в трекер будет в разы быстрее, чем сначала искать «кто, где и зачем».

      По поводу того, что комментарии не поддерживают — не соглашусь.
      Я бы посмотрел на того, кто бы взялся сопровождать тот же bb, не будь он так хорошо задокументирован)
      Если комментарии не нужны, то почему так много рекомендаций из самой различной литературы и интернета отмечают умение грамотно писать комментарии одним из правил хорошего тона?


      1. Zapped
        07.04.2017 18:55
        +1

        Когда мы делаем git blame в каком-нибудь огромном файле

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

        это ничем не отличается от наличия/отсутствия (обсуждаемого нами) коммента

        это вопрос дисциплины и гайдов оформления текста коммита, только вот в отличие от отсутствия комментария с причинами «забыл/лень/не хочу/не знаю» текст коммита легко валидируется хуками, что способствует гайдам

        просто вбить заранее указанный номер задачи в трекер будет в разы быстрее

        с «быстрее вбить» спорить даже не собираюсь (я сам люблю one-click переходы)
        но Вы забываете про достоверность

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

        я не знаю, что такое bb
        но с высоты своего опыта поддержки многолетних активно меняющихся legacy-продуктов могу утверждать, что такие комментарии бессмысленны чуть менее, чем совсем

        отмечают умение грамотно писать комментарии одним из правил хорошего тона

        я выделил ключевое слово ;)


      1. alexeykuzmin0
        07.04.2017 23:07
        +2

        Соглашусь с Zapped. Очень уж часто у моей практике такие комментарии протухали. Или трекер менялся, и получалось как на bash:

        комментарий перед злобной реализацией некого алгоритма на несколько страниц: «описание алгоритма смотри в тетрадке у Чуня».


  1. Zapped
    07.04.2017 18:54

    del (не туда)