Pull to refresh

К вопросу о стиле

Reading time 21 min
Views 6.5K
Путь в десять тысяч ли начинается с первого шага.

Преамбула


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

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

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

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

Потому изложение не всегда будет достаточно строгим, чаще я буду обозначать проблему, не доказывая ее значимость, а лишь утверждая, и показывать возможные пути решения таковой. Тем не менее, полученные результаты будут безусловно верны, и, более того, вполне применимы в повседневной практике. Поскольку теоретические рассуждения сами по себе менее интересны, нежели конкретные примеры использования данной теории (и, более того, сложнее в написании и в прочтении), то мы с Вами, уважаемый читатель, займемся проектированием библиотеки, опираясь на ранее указанную.

Какие особенности мы должны учитывать при написании библиотеки, то есть продукта, который предназначен для дальнейшего использования не нами. Прежде всего, ее следует писать с особой аккуратностью, чтобы не было стыдно смотреть в глаза коллегам. «Как писать книги для детей? Также, как и для взрослых, только еще лучше». Конечно, стиль «Фигак, фигак и в продакшн» приобрел в последнее время определенную популярность в определенных кругах, но мы с Вами не поддадимся зову сирен, зовущих нас покинуть корабль надежности ради сиюминутных выгод. Кроме того, не следует забывать и о самом главном читателе нашего кода (нет, это не президент), а именно о самом себе. Выражения типа «интересно, а почему я тут использовал ...» произносятся намного чаще, чем хотелось бы, ведь, если у нас получится удачная библиотека (а как же иначе) и она будет востребована многими пользователями, то Вам придется иногда заниматься ее модификацией, и хотелось бы делать это без применения богатой лексики русского языка, особенно его ненормативной части. Но это общие соображения, и они должны быть справедливы для любой разработки, жаль, что это не всегда выполняется. Поэтому прежде, чем приступить к собственно написанию кода, следует определиться с существенными обстоятельствами, его сопровождающими.

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

Необходимое пояснение по поводу замечания в скобках — я вовсе не являюсь убежденным противником языка С, а тем более врагом его, но, поскольку мое становление, как программиста, связано с языком Pascal (вообще то, если быть честным, то с языком АЯП в машине «Наири» — ах эти замечательные директивы «введем» и «кончаем» — тогда я понял, что директивы на русском языке не настолько хорошая идея, как раньше казалось, а затем с Fortran, но боюсь, что слишком многие читатели об этих языках не знают, да и рекомендовать их в настоящий момент я не рискну, а Pascal вполне могу) то явление запечатления имеет место, и мне было некомфортно, когда я на личном опыте убедился в длине веревки, предоставляемой С (а она действительно намного длиннее помочей, на которых Вас держит Pascal, ну это и понятно, Вы уже выросли и способны самостоятельно передвигаться без поддержки). Но все-таки, С как первый язык, Вы хотели бы такого обучения ходьбе для своих детей, нег?

Перечисления против определений


Так вот, возвращаемся к нашим баранам, то есть типам и Вы, возможно, удивитесь, но тут действительно есть о чем поговорить. Для начала откроем файл… и сразу же натолкнемся на объявление пользовательских типов, которые автор планирует активно использовать в библиотеке, и сразу же кое-что из написанного мне не нравится. В данном случае я говорю о введении логического типа. Это не ошибка в принятом значении этого слова, но это большая ошибка в рамках принятой мною парадигмы программирования. Я считаю, что компиляторы пишут неглупые люди, и не следует считать себя намного более продвинутым по части использования языка, нежели они. Классическая реализация с дефайнами логических констант и использованием типа char приведена в данном файле, я противопоставлю ей реализацию через перечислимый тип, а именно:

typedef enum {False=0,True=1} Boolean;
Boolean IsOk=True;
...;
if (IsOk==False) ...;

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

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

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

Следующий плюс — компилятор Вам сам подберет необходимый примитивный тип для реализации, и перечислимые типы совершенно необязательно будут храниться в виде целого, если для хранения значения достаточно 8 бит. У меня IAR именно так и сделал, разместив переменные моего логического типа (типа перечисления) в байте. Вообще говоря, если уж мы хотим сами определить наш логический тип напрямую, то следует использовать не тип char, а at_least8_t или же fast8_t, а последние два никак не обязаны с байтом совпадать, хотя и могут. Более того, я не исключаю реализации компилятора, которые способны разместить логическую переменную в более подходящих для них местах, например, адресуемых битах в памяти, что несомненно эффективнее и по памяти и по быстродействию, чем самодельное определение.

Явные сравнения


Обратим внимание на строку, в которой написан оператор сравнения, и я категорически настаиваю именно на варианте с явно написанным сравнением в противовес с устоявшейся практикой «Что есть истина? — Не ноль», поскольку этот вариант явно указывает на требуемое условие, и не оставляет места для домыслов. Ну а если мы учтем, что 99% современных компиляторов (я бы написал ''все", но мало ли чего не бывает) увидев сравнение с нулем, не будут генерировать код для его осуществления (вернее, будет, но ровно такой же, как и для отсутствующего сравнения в в условии if (!error), то по эффективности мы не проигрываем. Ну и в завершение еще одно соображение — избегать переменных с именами типа NoErr (надеюсь, то, что наименование переменной должно совпадать со смыслом хранящегося ней значения, у Вас сомнений не вызывает, иначе у нас большие проблемы), поскольку возникают возможности разночтения и недопонимания выражений типа if (NoErr==False), а их следует избегать любыми способами и переменная IsOk в этом смысле предпочтительнее.

Также желательно писать условия в форме утверждения, то есть (IsOk == False) предпочтительнее, нежели (IsOk |= True), при этом вариант с ~(IsOk == True) даже не должен приходить в голову. Хотя есть люди, которые утверждают (хотя, может, это был троллинг, но для меня слишком тонко, есть вещи, шутить о которых неуместно), что вариант State|= !!ErrNumber << 5 эффективнее реализуется, нежели нормальное выражение if (ErrNumber! = 0) State = State | (1 << StateErrorBitNumber). Даже если бы это была правда, а на моем компиляторе код не получился ни короче, ни быстрее, то эта эффективность не стоила даже минутного ступора от подобной языковой конструкции.

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

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

БИТОВЫЕ ПОЛЯ


Кстати, по поводу битовых полей. Те, кто читал мои опусы, знают, а для остальных повторю еще раз — если Вы можете контролировать порядок байт в слове в своем компиляторе, то битовые поля являются поистине восхитительным способом работы с отдельными битами и битовыми полями как в регистрах, так и в обычных переменных и я настоятельно рекомендую такой возможностью пользоваться, поскольку выражение if (ErrNumber! = 0) State.ErrorBit = 1 еще более понятно, чем приведенное в предыдущем параграфе. Если же религиозные соображения либо психологическая травма, полученная в детстве, когда Вы неожиданно открыли файл, написанный Вашими родителями и увидели там этот неприкрытый прием, не позволяют Вам применять битовые поля и Вы предпочитаете битовые маски, то у меня есть настоятельная рекомендация — прячьте операции с ними за макросами и определяйте зависимые константы одну через другую, а не индивидуально.

В своих ранних постах я данный вопрос рассматривал, в комментариях даже есть макрос, позволяющий наряду с традиционным определением связанных констант типа #define StateBitNumber (12)
#define StateBitMask (1 << StateBitNumber)
использовать и обратное в стиле #define StateBitMask (0x00004000)
#define StateBitNumber (BITNUMBER(StateBitMask))
. Постарайтесь придумать сами реализацию последнего макроса, а можете и посмотреть. И, ради Бога, используйте только производные от этих констант, свое отношение к людям, позволяющим себе выражения типа *(uint *) 0x20008000 = *(uint*) 0x20008000 & 0xffff0fff | 0x00004000, причем подобное выражения с одинаковыми константами встречаются у них в тексте многократно, я уже выражал, помните о больших кухонных ножах.

Обратите внимание, что битовое поле в регистре — это ни в коем случае не логический тип, а целый, пусть и длиной в 1 бит, никогда не делайте его тип отличным от целого, а вот в слове состояния процесса (в переменной) вы вполне можете себе позволить и такую конструкцию — логический тип битовой длиной 1. И не забывайте о ветви else, которую я вполне сознательно упустил в своем примере, чтобы он полностью соответствовал критикуемому оператору, который может быть и правильным в соответствующем контексте. В то же время, я не ограничиваю Вас в праве написать что- то вроде ProcessOK = (Boolean) (((ReadOk==True) || (WriteOK==True)) && (ErrNum ==0)), поскольку в данном операторе явно выражено Ваше понимание происходящего и декларирована решимость взять на себя ответственность за возможные последствия.

Имена переменных


Еще одно незначительное, но в определенных кругах вызывающее споры обстоятельство, а именно имена переменных, будет затронуто постольку, поскольку оно существует. Как нетрудно видеть из моих примеров, у меня есть определенные предпочтения в данном плане, но я их никоим образом не навязываю, стиль с подчеркиваниями ничуть не хуже (хотя, наверное, все таки хуже, раз я его не использую), венгерская нотация распространена весьма широко и у нее есть свои преимущества, порядок объекта и признака возможен любой, так что единственная рекомендация — придерживаться одного стиля именования и избегать имен вроде IsOK6.

Лучше, если у Вас лично или в Вашей организации есть неофициально либо формально установленный стиль кодированияи именования переменныъ, поскольку любой, даже не очень хороший стандарт, лучше его отсутствия. На тему истинно правильного именования переменной можно спорить бесконечно и до хрипоты и перехода на личности, поэтому я еще раз скажу, что при Вы можете делать все, что угодно (при соблюдении определенных правил), и я сторонник формулировки «Да разбивает каждый истинно верующий яйцо с того конца, с которого ЕМУ удобнее», где выделена моя правка известной цитаты, хотя я совершенно не уверен, что даже такая правка спасла бы Лилипутию от междоусобной войны.

Вообще то, мы рассматривали преимущества и недостатки перечислимых типов, если кто еще не забыл, следуя за полетом моей причудливой фантазии, так что вернемся к данному вопросу. Мы разобрали плюсы предлагаемой реализации, убедились в отсутствии видимых минусов, но так не бывает, что-то должно было ухудшиться. «Читатель ждет уж рифмы розы — ну так лови ее скорей». Мы потеряли возможность интерпретации нашего перечислимого типа, как логического выражения, которое по дефолту представлено в С для встроенных целых типов, вернее, не совсем потеряли, но подобная попытка будет сопровождаться предупреждением компилятора, а мы договорились их не игнорировать.

Да, мы не сможем теперь написать очаровательное выражение типа if (~NoErr || !NoSuccess && NoRetry) ..., которое, несомненно доставит несколько малоприятных минут даже автору после полугодового перерыва, а про остальных даже и нечего говорить, но, поверьте, потеря не столь велика. А для выражения логических условий в понятной всем форме наш логический тип вполне приемлем, например if (IsOk==True) || ((Success==True) && (Rerty==False))) ..., где все просто и понятно, и даже если данная конструкция будет исполняться на 2 микросекунды дольше, в чем лично я не уверен, то оно того стоит. Как сказал кто-то из неглупых людей, мы пишем программы не для компилятора, а для других людей.

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

Выражайтесь ясно


Поэтому я настоятельно призываю Вас писать код насколько можно проще (известное правило KISS), которое постоянно нарушается, иногда из ложно понятого чувства профессиональной гордости (существует мнение, что чаще всего последняя запись на черном ящике звучит «смотри, как я умею»), но чаще просто по привычке. Читаю неплохую книгу «С 21го века», которую мне порекомендовали в комментариях к предыдущему посту, и вижу весьма привычное место static int Data=0; if (!Data) ...; и возникает у меня вопрос, с какой целью простое и понятное условие (Data==0) записано в таком виде, если мы решили по-извращаться, можно было бы написать и (~Data & 1) (данное выражение еще прикольнее и демонстрирует Ваше глубокое понимание поведения целых типов, а ведь это главная цель написания программы — блеснуть своими знаниями), а можно придумать и еще что-нибудь. Неужели это для того, чтобы экономить два символа при вводе текста а также место на диске при хранении файла? Конечно, все три варианта сработают идентично, и целый тип можно в С интерпретировать как условие, но, все-таки, зачем? Проверить знание читателем (пользователем) правил синтаксиса С — не надо, еще раз, у нас не собеседование.

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

Машинонезависимые типы


Идем далее по тексту программы и натыкаемся на очередное загадочное место, а именно на определения типов UCHAR и UINT. Почему загадочное место? Во-первых, непонятно, зачем эти типы вообще вводить, из их названия ровным счетом ничего не следует (мы с Вами договорились о значащих идентификаторах, воут?), во-вторых, из текста видно, что это просто алиасы для стандартных типов, а в-третьих, почему взяты стандартные типы, а не рекомендованные для применения во встроенном программировании типы с явным указанием размера. Неужели это всего лишь попытка набирать меньше символов при вводе текста?

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

Кстати, интересный вопрос для пытливого читателя — а как именно определены эти типы с явным указанием размера в реализации языка? Предлагаю на минуту задуматься, предложить варианты, а потом посмотреть код файла stdint.h в Вашем компиляторе. Лично я в своем IAR обнаружил алиасы для выражений вроде __INT8_T__, которые дальнейшему анализу не поддаются и, очевидно, должны быть реализованы собственно в компиляторе. Те, кто читал стандарт языка С (видите, как я бравирую собственной безграмотностью, и это уже не в первый раз), могут поправить меня в комментариях, но, похоже, что эти макросы внутренней реализации, как и в случае с оператором sizeof, реализацию которого я когда то искал, да так и не нашел, уперевшись в аналогичное выражение. А вот в Keil данные типы реализованы напрямую через стандартные, что заставляет задуматься, а как должно быть на самом деле, или реализация полностью отдана на откуп производителям компилятора?

Еще один вопрос для пытливого читателя — а какие типы мы вообще можем и должны использовать в случае необходимости явного указания размера? С одной стороны, нам рекомендуют применять ни в коем случае не (u)int8_t, реализация которого является опциональной, а fastint8_t, длина которого ограничена снизу, а вовсе не жестко задана, как и у типа atleast8_t. Конечно, опыт показывает что все виды типов с явным указанием размера реализованы во всех практически доступных компиляторах (мне неизвестны обратные случаи, а читатель уже, видимо, заметил мое стремление обобщать личный опыт), но все таки лучше перестраховаться, есть фраза «Лучше выкопать 100 метров окопа, чем 2 метра могилы». С другой стороны, последние два типа не гарантируют нам точную длину, в том ж Kеil они приравнены к короткому целому и никак с байтом не совпадают.

Мое решение — если мне действительно нужен байт, то тип char является правильным, поскольку sizeof(char)==1 жестко зафиксирован стандартом, а вот для целого длиной 16 бит следует применять скорее всего fast16_t, хотя это опять-таки ничего не гарантирует. Поэтому правильным решением будет описывать пакет информации в виде набора байтов, а превращение в внутренние переменные различных типов и обратно из них осуществлять явно прописанными операциями с массивом байтов.

Беззнаковые типы


Сразу же затрону тему беззнаковых чисел. Почему то сложилась практика, что для определении регистров внешних устройств (и при описании структуры пакетов передачи данных) применяют именно беззнаковые типы. Мне не очень понятно такое решение, поскольку единственное различие между знаковым и беззнаковым типом состоит в реализации операций > и <, для знаковых типов анализируется старший бит, а для беззнаковых — бит переноса, но мы ведь не собираемся проводить с ними такие операции. Небольшое различие есть также при преобразовании типов, которое приводит к незначительному изменению быстродействия (по крайней мере, для ARM камней), причем в разные стороны.

В тоже время, ни к каким отрицательным последствиям применение беззнаковых типов не приводит, за исключением того, что нам придется писать лишний символ u, а ведь это идет вразрез с основной парадигмой современного программирования, которая заключается в экономии места на диске, не так ли? Для полей, действительно содержащих информацию, которая интерпретируется как беззнаковое значение, например длину данных в пакете, причем она не может быть отрицательной по определению, применение беззнакового типа позволяет сделать эквивалентными выражения (Len > 0) и (Len != 0), а далее использовать более удобное из них, но для полей, не содержащих подобную информацию, беззнаковый тип не обязателен.

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

Структура файлов


Теперь немного о файлах. Не знаю, как Вам, но мне до сих пор (хотя прошел уже не один десяток лет) непривычно не найти в корневой директории проекта текстового файла с описанием структуры модулей и мест, где расположенные реализующие их файлы. Я понимаю, что скорее всего заголовочные файлы будут лежать в директории, название которой начинается с INC, а исходники в директории SRC, хотя тут уже есть варианты, вроде SOURCE, но в годы моей юности наличие такого файла считалось обязательным, не вижу оснований от подобной практики отказываться и текстовый файл, информирующий меня о авторе данного программного пакета и вида лицензии, под которой он был разработан, считаю совершенно недостаточным.

Как не так давно писал в своем блоге Джек Гансли, «когда в начале текста программы я вижу двухстраничное описание требований лицензии, на котором комментарии практически заканчиваются, у меня возникают сомнения в квалификации автора». Данное выражение относится к рассматриваемому продукту чуть менее, чем полностью. И не верьте сиренам, пропагандирующим Doxygen, он не напишет комментарии за Вас, вот подготовить описание структуры и состава файлов он действительно может, особенно, если Вы ему поможете, но комментарии — исключительна Ваша задача.

И раз уж зашел разговор о комментариях, то мой совет — не следует писать комментарии в стиле Counter ++; // Увеличиваем значение счетчика на единицу. Это не шутка, подобные комментарии частенько встречаются в программах, которые авторы не стесняются выкладывать на всеобщее обозрение. Недавно прочитал хорошую формулировку «Комментарии не должны оскорблять читателя». Правда, данное явление больше свойственно Ардуино среде, но, тем не менее, такой комментарий даже хуже его отсутствия, поскольку создают ложное ощущение о своем наличии. А на самом деле тут нет комментария, написана точно такая же операция, просто на немного другом языке программирования, и если существуют среди ожидаемых читателей кода люди, которым комментарии подобного рода помогут понять логику работы программы (а это и есть основное назначение комментария), то единственное, что я могу им посоветовать, это поменять вид деятельности и не связываться с областью, требования которой столь явно превосходят их умственные возможности.

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

Заголовочные файлы


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

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

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

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

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

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

Если у Вас действительно есть необходимость в начале файла разместить страницу (или две) с названиями включаемых заголовочных файлов, то, возможно, Ваш модуль слишком велик, и его следует декомпозировать, воут? Хотя в этом деле хорошо бы не переборщить и не создать 100500 мелких файлов с кодом на две строки исполнения. Истина, как всегда, расположена между двумя крайностями, есть у нее такое милое свойство — всегда лежать посередине (если из этой фразы Вы вычитали что-то сверх того, что в ней написано, Вам должно быть стыдно). Да, я готов согласиться с упомянутым оратором, во многих местах прямо таки напрашиваются вложенные заголовочные файлы, но, все-таки, они всего лишь удобны, а не обязательны, а, во-вторых, когда Вы в последний раз во встроенном программировании (а я пишу исключительно про этот раздел разработки ПО) реализовывали паттерн «Делегатор»?

Ключевые слова


Обсудим не менее важную тему — ключевые слова. Позволю себе длинную цитату из совершенно очаровательного рассказа «Энтропия, эргодический источник, многомерное пространство сообщений, биты, многосмысленность, процесс Маркова — все эти слова звучат весьма внушительно, в каком бы порядке их ни расположили. Если же расположить их в правильном порядке, они обретают определенное теоретическое содержание. И настоящий специалист порой может с их помощью найти решение повседневных практических задач». Так вот, мы будем применять ключевые слова осмысленно, то есть

volatile — лишь там, где оно действительно необходимо,
static — везде, где оно не помешает,
const — повсюду, где только можно,
auto и register — будем избегать, поскольку мы с Вами уже договорились, что мы не умнее компилятора, пусть сам справляется.

Прочие ключевые слова не столь значимы, но разумное применение директивы inline действительно может несколько повысить быстродействие, если мы не будем забывать об особенностях компилятора, директиву же intrinsic я настоятельно не рекомендую, поскольку не очень хорошо понимаю механизм ее работы и соответственно, смысл ее использования. Все остальные директивы следует применять с особой осторожностью, поскольку они, как правило, компиляторо-зависимы и могут как отсутствовать в других реализациях (как, например, нравящаяся мне прагма warm__unused_result в GNU компиляторе), так и функционировать несколько по другому. Поднятая тема позволяет нам плавно перейти к следующему абзацу, а именно настройке на среду исполнения.

Настройка на компилятор


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

Для начала, нам потребуется настройка на язык, в данном конкретном месте выражающаяся в виде директивы extern C, которая позволит программе на С++ использовать наши функции, оформленные в объектном файле по правилам языка С. Применение данной директивы совершенно необходимо, если мы передаем пакет в виде объектного файла либо библиотеки, но может быть исключено при использовании исходного текста для полной сборки. Я бы рекомендовал при полной сборке данную директиву отключать, но тут решать Вам, можно и оставить. Но сразу замечание по стилю — в исходном примере данная директива прописана напрямую в условном выражении, что лично мне не очень нравится. Я предпочитаю оформлять подобные директивы в виде отдельного включаемого файла, причем в нем же расположено управление реализацией директив и рекомендую именно такой подход.

Итоги


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

P.S.


Первая часть кода, построенная на основе следования вышеприведенным рекомендациям, будет расположена на Githab/tGarryC/Modbus/, когда я наконец то разберусь с Гит.
Tags:
Hubs:
+10
Comments 28
Comments Comments 28

Articles