До питання про стиль

Шлях у десять тисяч лі починається з першого кроку.

Преамбула
За результатами опитування в попередньому пості мені стало ясно, що значна частина тих, хто читає мої пости хотіла б прослухати курс про програмування вбудованих систем, тому пропоную наступний текст розглядати, як вступну лекцію. Інша, не така велика, але досить таки значна частина читачів декларувала можливість прочитати подібний курс самостійно, так що приєднуйтесь, не бачив поки на Хабре начерків Ваших лекцій, сподіваюся, вони скоро там з'являться.

У своєму недавньому (коли я починав писати, він дійсно був недавнім) пості про методи побудови модуля ініціалізації апаратної частини МК (до речі, кому цікава дана тема, обов'язково подивіться коментарі до нього, там дано абсолютно чудове рішення, яке, на жаль, не вмістилося на полях цього рукопису) я обіцяв присвятити наступний пост особливостям 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/, коли я нарешті то розберуся з Гіт.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.