Немає жодної переваги, яку я можу придумати (але див. Примітку до JasonS внизу), загортаючи один рядок коду як функцію чи підпрограму. За винятком того, що ви можете назвати функцію чимось "читабельним". Але ви можете так само добре прокоментувати рядок. А оскільки загортання рядка коду у функцію коштує пам'яті коду, місця у стеку та часу виконання, мені здається, що це здебільшого контрпродуктивне. У навчальній ситуації? Це може мати певний сенс. Але це залежить від класу учнів, їх попередньої підготовки, навчальної програми та викладача. Переважно, я думаю, що це не дуже гарна ідея. Але це моя думка.
Що підводить нас до суті. Ваша широка область питань десятиліттями була предметом дебатів і до сьогодні залишається предметом дебатів. Отже, принаймні, коли я читаю ваше запитання, мені здається, це питання, засновані на думці (як ви його задали).
Це може бути відсторонено від настільки ж обґрунтованого на думку думки, якби ви були більш детальними щодо ситуації та ретельно описали цілі, які ви поставили як основні. Чим краще ви визначите свої інструменти вимірювання, тим об'єктивнішими можуть бути відповіді.
Загалом, ви хочете зробити наступне для будь-якого кодування. (Нижче я припускаю, що ми порівнюємо різні підходи, усі з яких досягають поставлених цілей. Очевидно, що будь-який код, який не виконує необхідні завдання, є гіршим за успішний код, незалежно від того, як це написано.)
- Будьте послідовні щодо свого підходу, щоб чергове читання вашого коду могло розвинути розуміння того, як ви підходите до процесу кодування. Бути непослідовним - це, мабуть, найгірший можливий злочин. Це не тільки ускладнює оточуючим, але й ускладнює повернення коду через рік.
- Наскільки це можливо, спробуйте впорядкувати речі так, щоб ініціалізація різних функціональних розділів могла здійснюватися без огляду на замовлення. У разі необхідності замовлення, якщо це пов'язано з тісним з'єднанням двох сильно пов’язаних підфункцій, то розгляньте одну ініціалізацію для обох, щоб її можна було переупорядкувати, не завдаючи шкоди. Якщо це неможливо, задокументуйте вимогу впорядкування ініціалізації.
- Якщо можливо, інкапсулюйте знання саме в одному місці. Константи не слід дублювати всюди в коді. Рівняння, які вирішуються для певної змінної, повинні існувати в одному і лише одному місці. І так далі. Якщо ви виявите, що ви копіюєте та вставляєте деякий набір ліній, які виконують певну необхідну поведінку в різних місцях, розгляньте спосіб захоплення цих знань в одному місці та використання його там, де потрібно. Наприклад, якщо у вас є структура дерева, до якої потрібно ходити певним чином, не вартокопіювати кодовий код у кожному місці, де потрібно прокрутити дерева. Замість цього захопіть метод вигулу дерева по одному місці і використовуйте його. Таким чином, якщо дерево змінюється і змінюється метод ходьби, у вас є лише одне місце, де потрібно хвилюватися, а весь інший код "просто працює правильно".
- Якщо ви розкладете всі свої програми на величезний плоский аркуш паперу зі стрілками, що з'єднують їх так, як їх називають інші підпрограми, ви побачите, що в будь-якій програмі будуть "кластери" підпрограм, у яких багато і багато стрілок між собою, але лише кілька стрілок поза групою. Таким чином, існуватимуть природні межі тісно зв'язаних процедур і слабко пов'язаних зв'язків між іншими групами тісно пов'язаних процедур. Скористайтеся цим фактом, щоб упорядкувати свій код у модулі. Це істотно обмежить уявну складність вашого коду.
Вище сказане справедливо в цілому щодо кодування. Я не обговорював використання параметрів, локальних чи статичних глобальних змінних тощо. Причина полягає в тому, що для вбудованого програмування прикладний простір часто ставить крайні та дуже значні нові обмеження, і неможливо обговорити їх усіх, не обговорюючи кожну вбудовану програму. І це все одно не відбувається.
Ці обмеження можуть бути будь-якими (і більше) з них:
- Суворі обмеження витрат, що вимагають надзвичайно примітивних MCU з мізерною оперативною пам'яттю та майже відсутністю вводу-виводу. Для них застосовуються цілі нові набори правил. Наприклад, вам, можливо, доведеться писати у складальному коді, оскільки місця в коді не так багато. Можливо, вам доведеться використовувати ТОЛЬКІ статичні змінні, оскільки використання локальних змінних є занадто дорогим та трудомістким. Можливо, вам доведеться уникати надмірного використання підпрограм, тому що (наприклад, деякі частини Microchip PIC) є лише 4 апаратних регістри, в яких можна зберігати зворотні адреси підпрограми. Тож вам, можливо, доведеться різко «згладити» свій код. І т.д.
- Суворі обмеження потужності, що вимагають ретельно продуманого коду для запуску та вимкнення більшості MCU та встановлення суворих обмежень на час виконання коду при роботі на повній швидкості. Знову ж, це може вимагати деякого монтажу кодування часом.
- Суворі вимоги до термінів Наприклад, бувають випадки, коли мені доводиться переконуватись, що передача відкритого стоку 0 повинна мати ТОЧНО таку ж кількість циклів, що і передача 1. І що вибірку цієї ж лінії також потрібно було виконати з точною відносною фазою до цього часу. Це означало, що C тут НЕ можна використовувати. ТІЛЬКИ можливий спосіб зробити цю гарантію - це ретельно скласти код складання. (І навіть тоді, не завжди у всіх проектах ALU.)
І так далі. (Код підключення для життєво важливих медичних приладів також має цілий світ.)
Підсумок тут полягає в тому, що вбудоване кодування часто не є безкоштовним для всіх, де ви можете кодувати як на робочій станції. Часто існують важкі, конкурентні причини для широкого спектру дуже складних обмежень. І це може рішуче заперечувати проти більш традиційних та акціонерних відповідей.
Щодо читабельності, я вважаю, що код є читабельним, якщо він написаний послідовно, що я можу навчитися, читаючи його. І там, де немає навмисної спроби придушити код. Дійсно, не потрібно набагато більше.
Читаний код може бути досить ефективним, і він може відповідати всім вищезазначеним вимогам, про які я вже згадував. Головне, ви повністю розумієте, що кожен рядок коду, який ви пишете, виробляє на рівні складання або на машині, як це кодуєте. C ++ покладає тут серйозну навантаження на програміста, оскільки існує багато ситуацій, коли однакові фрагменти коду C ++ насправді генерують різні фрагменти машинного коду, які мають надзвичайно різну продуктивність. Але C, як правило, є переважно мовою "те, що ти бачиш, те, що ти отримуєш". Так що в цьому плані безпечніше.
EDIT за JasonS:
Я використовую C з 1978 року, а C ++ - приблизно з 1987 року, і я мав великий досвід використання як для основних комп'ютерів, мінікомп'ютерів, так і для (в основному) вбудованих додатків.
Джейсон демонструє коментар щодо використання "inline" в якості модифікатора. (На мій погляд, це відносно "нова" здатність, оскільки вона просто не існувала, можливо, половину мого життя або більше, використовуючи C і C ++.) Використання вбудованих функцій може насправді робити такі дзвінки (навіть для однієї лінії код) цілком практичний. І куди краще, де можливо, ніж використовувати макрос через типізацію, яку може застосувати компілятор.
Але є і обмеження. Перший полягає в тому, що ви не можете покластися на компілятора, щоб "взяти підказку". Це може бути, а може і ні. І є вагомі причини не брати підказки. (Для очевидного прикладу, якщо адреса функції взята, для цього потрібна інстанція функції, а використання адреси для здійснення дзвінка вимагатиме виклику. Код не може бути накреслений тоді.) інші причини. Компілятори можуть мати найрізноманітніші критерії, за якими вони судять, як поводитися з підказкою. А як програміст, це означає, що ви повинніприділіть деякий час вивченню цього аспекту компілятора, інакше ви, ймовірно, будете приймати рішення, грунтуючись на хибних ідеях. Таким чином, це додає тягар як письменнику коду, так і будь-якому читачеві, а також тому, хто планує перенести код до іншого компілятора.
Також компілятори C і C ++ підтримують окрему компіляцію. Це означає, що вони можуть скласти один фрагмент коду С або С ++ без складання будь-якого іншого пов'язаного коду для проекту. Для вбудованого коду, якщо припустити, що компілятор може інакше зробити це, він не тільки повинен мати декларацію "в області", але і він повинен мати визначення. Зазвичай програмісти працюватимуть над тим, щоб це було в тому випадку, якщо вони використовують "inline". Але легко помилитися в помилках.
Взагалі, хоча я також використовую рядки там, де я вважаю це доречним, я схильний вважати, що не можу на нього покластися. Якщо продуктивність є суттєвою вимогою, і я думаю, що в ОП вже чітко написано, що було досягнуто значного посягання на ефективність, коли вони пішли на більш "функціональний" маршрут, то я, безумовно, вирішив би не покладатися на інлайнер як практику кодування і натомість слід дотримуватися дещо іншого, але цілком послідовного зразка написання коду.
Заключна примітка про те, що "вбудований" та визначення "в області" для окремого етапу компіляції. Можна (не завжди надійно) виконувати роботи на етапі зв’язки. Це може статися, якщо і лише тоді, коли компілятор C / C ++ поховає достатню кількість деталей у файлах об'єктів, щоб дозволити лінкеру діяти на запити "вбудованих". Я особисто не відчував системи зв’язків (за межами Microsoft), яка підтримує цю можливість. Але це може статися. Знову ж, на те, чи слід покладатися, залежатиме від обставин. Але, як правило, я вважаю, що це не переносилося на лінкер, якщо я не знаю інакше на основі хороших доказів. І якщо я на це покладаюся, це буде зафіксовано на чільному місці.
C ++
Для тих, хто цікавиться, ось приклад того, чому я залишаюся досить обережним щодо C ++ під час кодування вбудованих додатків, незважаючи на його готовність сьогодні. Я викину деякі терміни, які, на мою думку, всі вбудовані програмісти C ++ повинні знати холодно :
- часткова спеціалізація шаблонів
- vtables
- віртуальний базовий об'єкт
- кадр активації
- кадр активації розмотати
- використання розумних покажчиків у конструкторах та чому
- оптимізація повернення
Це лише короткий список. Якщо ви ще не знаєте всього про ці терміни і чому я їх перерахував (і багато іншого я тут не перераховував), то я б радив не використовувати C ++ для вбудованих робіт, якщо це не варіант для проекту .
Давайте коротко розглянемо семантику винятків C ++, щоб отримати лише аромат.
AB
A
.
.
foo ();
String s;
foo ();
.
.
A
B
Компілятор C ++ бачить перший дзвінок до foo () і може просто дозволити, щоб відбувся звичайний кадр активації, якщо foo () кидає виняток. Іншими словами, компілятор C ++ знає, що в цей момент не потрібен додатковий код для підтримки процесу розмотування кадрів, що бере участь в обробці винятків.
Але після створення String s, компілятор C ++ знає, що він повинен бути належним чином знищений до того, як можна буде дозволити розмотування кадру, якщо пізніше станеться виняток. Отже, другий виклик foo () семантично відрізняється від першого. Якщо другий виклик foo () кидає виняток (який він може робити, а може і не робити), компілятор повинен розмістити код, призначений для обробки знищення String s, перш ніж дозволити звичайному кадру розкручуватися. Це відрізняється від коду, необхідного для першого дзвінка до foo ().
(Можна додати додаткові прикраси в C ++, щоб зменшити цю проблему. Але факт полягає в тому, що програмісти, що використовують C ++, просто повинні бути набагато більш обізнаними про наслідки кожного рядка коду, який вони пишуть.)
На відміну від C malloc, новий C ++ використовує винятки для сигналізації, коли він не може виконувати необмежену пам'ять. Так буде "динамічний_каст". (Див. 3-е видання Stroustrup, Мова програмування на C ++, стор. 384 та 385 для стандартних винятків у програмі C ++.) Компілятори можуть дозволити відключення такої поведінки. Але в цілому ви будете мати певні накладні витрати через правильно сформовані винятки, що обробляють прологи та епілоги в згенерованому коді, навіть коли винятки насправді не мають місце і навіть тоді, коли складена функція насправді не має блоків обробки виключень. (Stroustrup про це публічно поскаржився.)
Без часткової спеціалізації шаблонів (не всі компілятори C ++ підтримують її) використання шаблонів може призвести до катастрофи для вбудованого програмування. Без цього розквіт коду - це серйозний ризик, який міг би вбити вбудований проект з невеликою пам’яттю.
Коли функція C ++ повертає об'єкт, неназваний компілятор створюється тимчасово і знищується. Деякі компілятори C ++ можуть надати ефективний код, якщо конструктор об'єктів використовується в операторі return, а не локальному об'єкті, зменшуючи потреби побудови та знищення одним об'єктом. Але не кожен компілятор робить це, і багато програмістів на C ++ навіть не знають про цю "оптимізацію повернутого значення".
Надання конструктору об’єктів одного типу параметрів може дозволити компілятору C ++ знаходити шлях перетворення між двома типами абсолютно несподіваними способами для програміста. Така "розумна" поведінка не є частиною C.
Застереження про вловлювання, що вказує базовий тип, "скине" викинутий похідний об'єкт, тому що викинутий об'єкт копіюється за допомогою "статичного типу" пункту лову, а не "динамічного типу об'єкта". Не рідкісне джерело нещасних випадків (коли ви відчуваєте, що можете навіть дозволити собі винятки зі свого вбудованого коду.)
Компілятори C ++ можуть автоматично генерувати для вас конструктори, деструктори, конструктори копій та оператори присвоєння із непередбачуваними результатами. Потрібен час, щоб отримати об'єкт з деталями цього.
Передача масивів похідних об'єктів у функцію, що приймає масиви базових об'єктів, рідко генерує попередження компілятора, але майже завжди призводить до неправильної поведінки.
Оскільки C ++ не викликає деструктор частково побудованих об'єктів, коли в конструкторі об'єктів відбувається виняток, обробка винятків у конструкторах, як правило, вимагає "розумних покажчиків", щоб гарантувати, що сконструйовані фрагменти в конструкторі належним чином знищені, якщо виняток виникає там . (Див. Stroustrup, стор. 367 і 368.) Це поширена проблема написання хороших класів на C ++, але, звичайно, цього уникати, оскільки C не має вбудованої семантики побудови та руйнування. Написання правильного коду для обробки конструкції підпредметів всередині об'єкта означає написання коду, який повинен впоратися з цією унікальною семантичною проблемою в C ++; Іншими словами "написання навколо" C ++ семантичної поведінки.
C ++ може копіювати об'єкти, передані параметрам об'єкта. Наприклад, у наступних фрагментах виклик "rA (x);" може змусити компілятор C ++ викликати конструктор для параметра p, щоб потім викликати конструктор копії для передачі об'єкта x до параметра p, а потім інший конструктор для об'єкта повернення (неназваного тимчасового) функції rA, що, звичайно, є скопійовано з параметра p. Гірше, якщо клас A має власні об'єкти, які потребують будівництва, це може катастрофічно телескопом. (Програміст змінного струму дозволить уникнути більшості цього сміття, оптимізуючи руку, оскільки програмісти C не мають такого зручного синтаксису і повинні викладати всі деталі по черзі.)
class A {...};
A rA (A p) { return p; }
// .....
{ A x; rA(x); }
Нарешті, коротка примітка для програмістів C. longjmp () не має портативної поведінки в C ++. (Деякі програмісти на C використовують це як своєрідний механізм "винятку".) Деякі компілятори C ++ насправді намагаються налаштувати речі на очищення під час зйомки longjmp, але така поведінка не переноситься на C ++. Якщо компілятор очищає побудовані об'єкти, він не є портативним. Якщо компілятор не очищає їх, то об'єкти не руйнуються, якщо код покидає область побудованих об'єктів внаслідок longjmp і поведінка недійсна. (Якщо використання longjmp в foo () не залишає сферу застосування, то поведінка може бути нормальною.) Це не занадто часто використовується вбудованими програмістами C, але вони повинні знати про ці проблеми, перш ніж використовувати їх.