Pull to refresh

Comments 71

Всё-таки такой заголовок выглядит прямой отсылкой к сцене из GoT… :)

Killing Bug
image
Меня это всегда умиляет — создать/использовать язык, в котором все сделано для максимального удобства простреливания ноги, а потом городить костыли, чтобы если уж прострелить, так только мягкие ткани, не раздробив кости.
Позволю себе маленькую цитату " Языки программирования делятся на две категории — одни, которые все ругают, и другие, на которых никто не работает ".
С и С++, несомненно, относятся к первой.
Стоп, а есть языки, которые никто не ругает?
другие, на которых никто не работает

Собственно, потому и не ругают. ))
Некоторые языки ругают даже те, кто на них не пишет, заочно так сказать)
Языку C уже более 40 лет, а достойной замены до сих пор нет. На чём ещё писать ОС, кодить микроконтроллеры и т.п.?
ИМХО: Этим правилам НУЖНО следовать лишь команде, начинающей свой путь в программировании. Любому более-менее опытному специалисту они будут лишь мешать. Ошибки вызванные «неповиновением» в большинстве своем надуманы.
А чем они будут мешать? Я бы сказал, что с опытом у программиста вырабатывается набор правил которому он старается следовать. И некоторые из приведенных правил вполне осмысленные. Они упрощают чтение кода, а в промышленном программировании полагаться на то, что твой код будут поддерживать только опытные разработчики, думаю не стоит.
Язык программирования — тоже язык. Хороший код не просто решает поставленную задачу, читая его должно приходить понимание того, ЧТО хотел донести автор. Многочисленные const, static, дефайны вместо комментирования и прочее несколько запутывают при чтении. Появляются дополнительные акценты там, где они не очень-то и нужны.
Вы, должно быть, шутите. Вот эта фраза
Многочисленные const, static, дефайны вместо комментирования и прочее несколько запутывают при чтении.
вызывает у меня эфект «гусиной кожи», когда я представляю код, написанный вами согласно этой фразе. Я так понимаю, вы предпочитаете сознательно лишать себя и других возможности переложить часть рутины на компилятор. Т.е. вы для аргументов функции вместо const A* a предпочитаете писать просто A*, а потом просто запоминаете, что эта функция не меняет память по указателю и ей можно доверять?
>>Язык программирования — тоже язык
>>читая его должно приходить понимание того, ЧТО хотел донести автор
>>Многочисленные const, static, дефайны вместо комментирования и прочее несколько запутывают при чтении

Сами себе противоречите.
Позволю себе ещё немного раскрыть мысль.

Я не к тому, что правила в корне неверны, а к тому, что ими нельзя слепо пользоваться ВЕЗДЕ. Как говорится — научи дурака богу молиться…

Так:
if (case_one)
{
	some = one;
}
else if (case_two)
{
	some = two;
}
else if (case_three)
{
	some = three;
}
else if (case_four)
{
	some = four;
}
else
{
	do_nothing();
}


Вместо:
     if (case_one  ) some = one  ;
else if (case_two  ) some = two  ;
else if (case_three) some = three;
else if (case_four ) some = four ;
esle
	do_nothing()


Так:
std::vector<const std::string>::reverse_const_iterator left;
std::vector<const std::string>::reverse_const_iterator right;
std::vector<const std::string>::reverse_const_iterator middle;

Вместо:
std::vector<std::string>::reverse_const_iterator
	left, right, middle;


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

и тому подобное.
У меня такое ощущение, что мы с вами говорим о разном.

Я с вами согласен,
Язык программирования — тоже язык. Хороший код не просто решает поставленную задачу, читая его должно приходить понимание того, ЧТО хотел донести автор
. Но почему же static или const запутывают код. Static перед функцией или переменной в Си, означает, что не нужно искать применение этих символов где то еще, то есть разработчику не нужно искать эти символы где то еще в проекте, достаточно пройтись по файлу. const в перед аргументом функции показывает, что данные не будут изменены. То есть, эти служебные слова несут дополнительную семантику, а это на мой взгляд всегда упрощают понимание кода.
На счет
#if 0
#endif

Опять же, автор указывает, что код не используется, но возможно будет когда то доработан. А комментарии для объяснения логики программы. Мне, например, тоже когда то это подсказали. Я подумал, погнусил и решил, что подобная дополнительная информация полезна.

Все это скорее не правила, а рекомендации, но в этих рекомендациях объяснены причины по которым они даются. Я не со всеми из них согласен, но многие полезны.

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

Мы в нашем проекте (для встроенных систем), когда то не очень сильно обращали внимание на предупреждения, было порядка 300 штук на 30000 строк кода, Но среди этих вот предупреждений попадались ошибки, или потенциальные проблемные ситуации. Пришлось включить при сборке предупреждения как ошибки. Было трудно и сейчас многие вещи раздражают, например, неиспользуемые переменные. Но в результате код стал лучше, стабильнее и мы уже пытаемся искать проблемы статическими анализаторами. Хотя конечно, еще работать и работать.
Ну уж нет! Выдающийся вправо первый if — это ж кошмар в плане code-style! Нельзя так писать! Да и действие в той же строчке, что и if — тоже та ещё радость. Правильный (утрированный) вариант с точки зрения code-style это:
if (case_one) {
    doOneThing();
} else if (case_two) {
    doSecondThing();
} else if (case_three) {
    doThirdThing();
} else if (case_four) {
    doFourthThing();
} else {
    doDefaultThing();
}
Согласен.
Тот вариант с выдвигающимся else из разряда поэзии в коде. Эстетически красиво, но для промышленного программирования не очень подходит. Но я написал как раз о том, что подобные места с множеством вложенных if else уже обращают на себя внимание и на мой взгляд, стиль форматирования в данном контексте менее значим.

Хотя конечно был у меня один прецедент.
Один студент ACM-щик, пришел ко мне и радостно сообщил, что написал приоритетную очередь в одну строчку да еще без тела цикла for. Причем заявлял, что это круто. Ну получил по шапке и пошел писать как нормальные люди:)
Че-то я напрягся насчет volatile. Глобальные переменные, изменяемые в прерывании, должны быть volatile?!
Ну, если подумать, то да. Стандарты C и C++ (до редакции 11 года) описывают абстракную вычислительную машину, которая последовательно выполняет инструкции. Поэтому, с точки зрения стандарта, эти функции эквивалентны:

bool run = true;

void func1()             void func2()
{                        {
  while(run)             again:
  {                     
    led_blink();           led_blink();
  }                        goto again;
}                        }


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

Не эквивалентны, пока компилятор не может доказать, что led_blink() не изменяет run.
Естественно. Но мы-то обсуждаем конкретный случай, когда неожиданно может прийти прерывание и выставить run в false.
Это не делает эти функции эквивалентными с точки зрения стандарта.
В этом примере led_blink априори не меняет переменную run.

Для полноты картины давайте добавим:

#define led_blink() (PORTB ^= (_BV(0)))
Несомненно.
Если Вы найдете презентацию Барра под названием «AppKiller», то в разборе случаев с превышением дозы облучения, показано, что изменение переменной в другой задаче привело к катастрофическим последствиям. Хотя там volatile и не спас, тем не менее хорошая иллюстрация в тему.
UFO just landed and posted this here
А как вы тогда времмено код выключаете? Настоящим условием?
UFO just landed and posted this here
О static не знаю, меня статические методы\поля немного пугают.

Уверен, речь не шла о С++-специфичном использовании static в классах.
Правило 7 непонятно. Как они рекомендуют распространять знак при вырезании битового фрагмента числа?
int32_t y=(x<<26)>>29; // get bits 3..5 with extended sign
— это же идиома?
Да и просто превратить число в 0/-1 с помощью x>>31… и далее — первая глава Hackers Delight в полном объёме…
int32_t y=(x<<26)>>29; // get bits 3..5 with extended sign
— это же идиома?

Эта идиома не переносима. Переносимый вариант для данного случая выглядит так:
int32_t y = ((((uint32_t)x >> 3) & ((1u << 3) - 1)) ^ (1u << 2)) - (1u << 2);

т.е. сдвинуть — наложить маску — инвертировать знаковый бит — отнять знаковый бит.
А существуют ли сейчас процессоры, на которых знаковый сдвиг не работает? И есть ли стандартный #define, позволяющий их опознать?
Процессоры тут ни при чём. Нельзя и всё. По стандарту. Сдвиг отрицательных чисел приводит к неопределённому поведению. Подробности.
Но если мне в самом деле нужно, чтобы эта операция занимала два такта, а не 4? А иначе внутренний цикл будет слишком медленным (или не хватит регистров на конвейерную обработку)?
Если нужна точность до такта, надо использовать ассемблер, а не Си.
Ну и кстати, современные компиляторы сполне справляются.

Приведенное выше переносимое выражение
uint32_t f(uint32_t x)
{
  return (((x >> 3) & ((1u << 3) - 1)) ^ (1u << 2)) - (1u << 2);;
}
с помощью GCC компилируется в
f(unsigned int):
	movl	%edi, %eax
	sall	$26, %eax
	sarl	$29, %eax
	ret
Поэкпериментировать можно тут gcc.godbolt.org
Интересно, составляется ли где-нибудь список рекомендуемых для подобных ситуаций переносимых паттернов, которые программистам следовало бы использовать, а компиляторам — распознавать и правильно оптимизировать.
Хотя есть и второй вопрос — многие ли программисты пойдут проверять, справляется ли конкретный компилятор с поставленной задачей. Думаю, что 0.1% было бы слишком оптимистичной оценкой…
Сдвиг отрицательных чисел приводит к неопределённому поведению.

Только при сдвиге влево. При сдвиге вправо — implementation-defined.
Я о таких процессорах ничего не знаю. Думаю, что основная проблема не в том, что этот код можно скомпилировать под такой процессор, а в том, что оптимизирующие компиляторы под нормальные процессоры пользуются всеми возможными лазейками возникающими из-за (по стандарту) непоределённого поведения.
То есть, на процессоре, где нет арифметического сдвига вправо, компилятор имеет право использовать вместо него логический — даже для знаковых чисел. Звучит разумно…
А насчёт первого правила — кто мешал написать
if(5==foo) bar();

?
Сэкономили бы ещё одну строчку, и шансов случайно закомментировать вызов не было бы.
Речь не идет о случайном закомментировании, а о временном отключении функциис с целью отладки, которая в силу неподходящего стиля приводит к неожиданному поведению программы…
Честно говоря, я и сам частенько пишу так, как вы предложили, но мы же говорим о том, как ПРАВИЛЬНО писать программы, а не о девиациях в поведении отдельных разработчиков, вызваных их тяжелым прошлым )).
… как известно, мы пишем программы не для компьютеров, а для других людей, и мне, например, значительно понятнее код, когда я его могу увидеть весь на экране без необходимости роллирования.


А вот интересно — это сарказм, или автор писал серьёзно? Потому что я полностью согласен с этим утверждением, но то, что сейчас понимается под «правильным написанием программ», насколько я вижу, делает его внутренне противоречивым.
Был такой случай…

// где-то в хедере
#ifdef DEBUG
# define DBG_OUTPUT(x) Logger::putline((x));
#else
# define DBG_OUTPUT(x)
#endif

// где-то в коде
if (count > 500) DBG_OUTPUT("Too many entries!")
for (int i = 0; i < count; ++i)
{
//...
}

Обратите внимание: здесь абсолютно не важно, будет ли макрос стоять на той же строке, что и условный оператор. В итоге все гадали: почему в отладке всё работает, а в релизе творятся непонятные вещи?

Когда разобрались, ввели правило номер 1 из статьи.
Красиво. Но не очень очевидно, не выстрелит ли такое несоответствие где-нибудь ещё. Может быть, правильнее
# define DBG_OUTPUT(x) ;
или
# define DBG_OUTPUT(x) {}
чтобы макрос всегда раскрывался в оператор? А то «правило 1» здесь выглядит, как попытка замаскировать ошибку, сделанную в другом месте.
Идиома —
do { <опциональный код здесь> } while (0)

— чтобы невозможно было не поставить точку с запятой после.
А если просто
#ifdef DEBUG
# define DBG_OUTPUT(x) Logger::putline((x))
#else
# define DBG_OUTPUT(x)
#endif
— чтобы нужно было писать
DBG_OUTPUT(x);

Есть ли здесь пример, когда можно не поставить точку с запятой?

Только если сам Logger::putline((x)) кривой. Здесь бы скобочки круглые вокруг для спокойствия.
Я согласен, в итоге макросы тоже были поправлены. И ещё введено правило: после макросов ставить точку с запятой:
if (count > 500) DBG_OUTPUT("Too many entries!");

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

Удачи и упорства в этом деле! Из личного опыта: мелкие домашние проекты как раз лучше писать максимально правильно, ибо «сроки не горят», а опыта набираться надо. Так же можно на домашних проектах тренироваться в TDD, к примеру.
Надо делать над собой усилие и переходить к правильному стилю

После этого возникнет риск, что некоторые программы не сможет сопровождать уже никто. В первую очередь — алгоритмы с неустранимо сложной логикой, которые делением на функции/классы и прочим структурированием можно только усложнить. Пока их код на экране — глаз может проследить взаимосвязи и переходы. Если станет больше — никакой памяти не хватит, чтобы уловить, что из чего и почему следует. Даже если все методы будут иметь 30-буквенные имена, а каждая фигурная скобка будет на своей строчке.
Просто нужно знать меру в наведении красоты ;)
Вот именно. Не понимаю людей, которые думают в чёрно-белом стиле. В C есть указатели, через которые можно поменять любую область памяти приложения. Значит ли это, что указателями нельзя пользоваться? Нет, просто нужно это делать осторожно.
Правило 2 вовсе не универсально: const это своеобразный макрос — компилятор (обычно) проставляет в код конкретные значения.
Т.е. потребление памяти скорее увеличится. И чревато сюрпризами с модульностью.
Т.е. потребление памяти скорее увеличится.
Каким образом? Непосредственные (immediate) значения обычно кодируются прямо в инструкциях

И чревато сюрпризами с модульностью.
Что вы имеете ввиду под модульностью, и какие проблемы с ней могут возникнуть?
Потому что значение редко когда меньше указателя по размерности.

Если не пересобирать весь проект целиком не получается быть уверенным, что константа во всех частях программы имеет одно значение.
#define SQUARE(A) ((A)*(A))
inline uint32_t square(uint16_t a)
{
  return (a * a);
}

Автор сам же пренебрегает своими правилами (Правило 3). Для замены макроса лучше использовать не просто inline, а static inline. Иначе компилятор не только подставит функцию как текст куда просили, но сгенерирует для неё тело, которое будет доступно для линковки из всего проекта.
Это верно.
Кстати на моем IAR inline без static вообще не работает — падает при линкованиии (
Просто пример был именно таков в исходной информации — решил не трогать, вдруг в этом сакральный смысл есть?
на моем IAR inline без static вообще не работает — падает при линкованиии

Вполне резонно для любого компилятора, если inline в *.h подключенном более чем в один *.с
Не для любого. inline -функции не подчиняются one definition rule, и в этом-то главная засада: если есть несколько не-static inline-ов с одной сигнатурой, но разными реализациями, линковщик может связать вызов с неправильной реализацией, и это куда хуже, чем «падение» линковки.
volatile лучше вообще не трогать. В обычном user-space коде оно не нужно, и резко ограничивает возможности компилятора по оптимизации. Проблемы с потоками решаются блокировками и TLS.
Сюда бы вот что добавить (касательно программирования на языке C для микроконтроллеров):

11. Старайтесь использовать типы данных минимального размера, соответствующие архитектуре микроконтроллера.
12. Вместо констант, заданных через #define, старайтесь использовать перечисляемый тип enum.
13. Не пренебрегайте битовыми полями — при надлежащем использовании это экономит ресурсы как процессора, так и рабочее время и нервные клетки программиста.
14. Проверяйте надежность кода, сравнивая его работу под разными уровнями оптимизации. Правильно написанный код должен корректно работать при любых настройках оптимизации. Если это не так, то есть повод задуматься.
15. В операторе if при проверке на равенство константе всегда ставьте константу влево, а переменную справа. Убиваем двух зайцев — получается код нагляднее, и Вы защищены от ошибочного присваивания (если вдруг вместо == нечаянно ввели =). Будьте внимательнее при вводе != и |=, потому что эти операторы визуально трудно отличить друг от друга, и компилятор скорее всего не оповестит Вас об ошибке.
16. Всегда обрамляйте сложные вычисляемые макросы (например, макросы с параметрами) круглыми или фигурными скобками (в зависимости от контекста).
17. Всегда обрамляйте параметр макроса круглыми скобками.
18. Старайтесь никогда не делать задержки на пустых циклах, разве что поступайте так в исключительно простых случаях.
19. Старайтесь писать программу для микроконтроллера так, чтобы она работала только на обработках событий. Т. е. в конце главного цикла main должен стоять оператор sleep, вводящий микроконтроллер в режим пониженного энергопотребления. Микроконтроллер должен просыпаться только в ответ на какое-то событие (истечение задержки таймера, завершение приема/передачи байта, блока DMA, преобразования АЦП, обнаружение изменения логического уровня, нажатие кнопки на клавиатуре и т. д.).
20. Используйте приведение типов только в исключительных случаях.
21. Избегайте множественных операторов return теле функции или процедуры. Этот оператор должен быть исключительно в конце функции/процедуры.
22. По возможности избегайте глобальных переменных. Если переменная глобальна для нескольких функций только одного модуля, то всегда её объявляйте как static. Но!.. В теле функции избегайте использовать static-переменные, по возможности заменяйте их на локальные переменные.
23. Не пишите const char * вместо char const *.
23. Не пишите const char * вместо char const *.

почему?
чаще в различных проектах я встречаю «const T», а не «T const»

В c# есть конструкция const char, но char const запрещена. В этом языке убрали неоднозначность в пользу первого варианта.
Наверное, поэтому char const мне режет глаз, как грамматическая ошибка — постоянно мозг встречает другой порядок, и запоминает его, как правильный.
В C# многое «не так», как привыкли видеть программисты C и C++. Куда подевались #define, typedef struct, union, указатели? Почему нельзя использовать заголовки? Почему const означает не совсем то, а static совсем не то, что раньше? Где наш любимый оператор printf и стандартные библиотеки для ввода/вывода и работы со строками и массивами памяти? Эх…
В операторе if при проверке на равенство константе всегда ставьте константу влево, а переменную справа. Убиваем двух зайцев — получается код нагляднее, и Вы защищены от ошибочного присваивания (если вдруг вместо == нечаянно ввели =).
Yoda style? Сомнительный совет.
  1. «5 == x» ну никак не нагляднее, чем «x == 5», вы сравниваете константу с переменной, что выглядит дико (если только вы не практикуете двоемыслие, которое является одним из симптомов шизофрении :)
    Это — самый обыкновенный костыль.
  2. Вместо того, чтобы писать подобные извращения, лучше использовать нормальный компилятор (GCC, например), который предупредит о возможной ошибке. Предупреждения компилятора вообще читать полезно.
  3. Можно себя просто приучить к внимательности. За последние несколько лет я ни разу не оставил присвоение в условии, я на автомате прыгаю глазами к участку кода, который только что написал. Также поможет просмотр изменений перед коммитом в репозиторий (без системы контроля версий могу позволить себе писать только HelloWorld) — очень полезная привычка, и лучше смотреть изменения утилитой типа meld (очень наглядно их отображает).
    Этот пункт — лишь дополнение к первым двум.
1. (5==x) не нагляднее, согласен. Никто не заставляет применять циферки вместо констант, правда? Можно ведь и if (_TROLL_ == burjui) написать. Или Вам больше нравится if (burjui = _COOL_PROGRAMMER_)? Компилятор IAR пропустит оба выражения, и даже не моргнет, без всяких предупреждений.

2. Почему вдруг компилятор GCC стал «нормальным», а другие компиляторы стали «ненормальными»? Это Вы сами придумали? Мне например нравится компилятор IAR, он генерит код намного эффективнее GCC. И кто хоть полслова сказал про то, что не надо читать предупреждения компилятора?.. Тут Вы не правы, не выдумывайте того, чего нет.

3. Приучайте себя к внимательности, весьма похвально. Прыгайте «глазами на автомате по коду», который только что написали, проверяйте себя на каждом шагу, наверное для глаз это полезное упражнение. Я стараюсь избегать обезьяньей работы, извините, и делать так никогда не буду.
1. Придираетесь к цифре, а смысл не меняется. IAR хорош, ничего не скажешь.
2. GCC предупреждает о присвоении в условии (см. п.1), а ещё потому, что нормально поддерживает стандарты и богат на диагностики (-Wall -Wextra).
3. Внимательность не нужна, вас понял.

Вообще, то меня с троллем сравниваете, то с обезьяной. Какая-то неадекватно-петросянская реакция на критику. Выспитесь.
Sign up to leave a comment.

Articles