Я не могу придумать никакого преимущества (но см. Примечание к JasonS внизу), заключая одну строку кода в функцию или подпрограмму. За исключением, возможно, того, что вы можете назвать функцию как-нибудь «читабельную». Но вы также можете прокомментировать строку. А поскольку завершение строки кода в функции стоит памяти кода, места в стеке и времени выполнения, мне кажется, что это в основном контрпродуктивно. В учебной ситуации? Это может иметь какой-то смысл. Но это зависит от класса учеников, их предварительной подготовки, учебной программы и учителя. В основном, я думаю, что это не очень хорошая идея. Но это мое мнение.
Что подводит нас к сути. Ваша широкая область вопросов в течение десятилетий была предметом некоторых дискуссий и до сих пор остается предметом некоторых дискуссий. Итак, по крайней мере, когда я читаю ваш вопрос, мне кажется, что это вопрос, основанный на мнении (как вы его задали).
Это можно было бы отойти от того, чтобы быть настолько же основанным на мнении, как если бы вы были более подробно о ситуации и тщательно описали цели, которые вы считали основными. Чем лучше вы определите свои инструменты измерения, тем более объективными могут быть ответы.
Вообще говоря, вы хотите сделать следующее для любого кодирования. (Ниже я предполагаю, что мы сравниваем различные подходы, которые все достигают целей. Очевидно, что любой код, который не выполняет необходимые задачи, хуже, чем код, который успешно выполняется, независимо от того, как он написан.)
- Будьте последовательны в своем подходе, чтобы другое чтение вашего кода могло развить понимание вашего подхода к процессу кодирования. Быть непоследовательным, вероятно, худшее из возможных преступлений. Это не только усложняет задачу для других, но и затрудняет возвращение к коду спустя годы.
- Насколько это возможно, постарайтесь организовать все так, чтобы инициализация различных функциональных разделов могла выполняться без учета порядка. Там, где требуется упорядочение, если оно связано с тесной связью двух тесно связанных подфункций, рассмотрите возможность одной инициализации для обоих, чтобы можно было изменить порядок, не причиняя вреда. Если это невозможно, задокументируйте требования к порядку инициализации.
- Инкапсулируйте знания в одном месте, если это возможно. Константы не должны дублироваться повсюду в коде. Уравнения, которые решают для некоторой переменной, должны существовать в одном и только одном месте. И так далее. Если вы обнаружите, что копируете и вставляете некоторый набор строк, которые выполняют необходимое поведение в разных местах, подумайте о том, как собрать эти знания в одном месте и использовать их там, где это необходимо. Например, если у вас есть древовидная структура, которую нужно пройти определенным образом, выполните нереплицируйте код обхода дерева в каждом месте, где вам нужно перебирать узлы дерева. Вместо этого соберите метод прогулки по дереву в одном месте и используйте его. Таким образом, если дерево меняется и метод ходьбы меняется, вам остается беспокоиться только об одном месте, а весь остальной код «просто работает правильно».
- Если вы разложите все свои подпрограммы на огромном плоском листе бумаги со стрелками, соединяющими их так, как они вызываются другими подпрограммами, вы увидите, что в любом приложении будут «группы» подпрограмм, которые имеют много-много стрелок между собой, но только несколько стрел вне группы. Таким образом, будут естественные границы тесно связанных подпрограмм и слабосвязанных соединений между другими группами тесно связанных подпрограмм. Используйте этот факт для организации вашего кода в модули. Это существенно ограничит кажущуюся сложность вашего кода.
Вышесказанное в целом верно для всего кодирования. Я не обсуждал использование параметров, локальных или статических глобальных переменных и т. Д. Причина в том, что для встроенного программирования пространство приложения часто накладывает экстремальные и очень существенные новые ограничения, и невозможно обсудить их все без обсуждения каждого встроенного приложения. И этого здесь не происходит, во всяком случае.
Этими ограничениями могут быть любые (и более) из них:
- Серьезные ограничения стоимости, требующие чрезвычайно примитивных микроконтроллеров с минимальной оперативной памятью и почти без подсчета выводов ввода / вывода. Для них применяются совершенно новые наборы правил. Например, вам, возможно, придется писать в ассемблерном коде, потому что не так много места для кода. Возможно, вам придется использовать ТОЛЬКО статические переменные, потому что использование локальных переменных слишком дорого и занимает много времени. Возможно, вам придется избегать чрезмерного использования подпрограмм, потому что (например, некоторые детали PIC Microchip) есть только 4 аппаратных регистра, в которых можно хранить адреса возврата подпрограмм. Так что вам, возможно, придется резко «сплющить» свой код. И т.п.
- Серьезные ограничения по мощности, требующие тщательно созданного кода для запуска и выключения большей части MCU, и наложение жестких ограничений на время выполнения кода при работе на полной скорости. Опять же, иногда это может потребовать некоторого ассемблера.
- Серьезные временные требования. Например, бывают случаи, когда мне приходилось следить за тем, чтобы передача 0 с открытым стоком потребовала ТОЧНО того же числа циклов, что и передача 1. И что выборка этой же линии также должна была быть выполнена с точной относительной фазой к этому времени. Это означало, что C не может быть использован здесь. Единственный возможный способ сделать эту гарантию - тщательно составить код сборки. (И даже тогда, не всегда на всех проектах ALU.)
И так далее. (Код проводки для жизненно важных медицинских приборов тоже имеет свой собственный мир.)
В результате встроенное кодирование часто не является бесплатным для всех, где вы можете кодировать, как на рабочей станции. Часто существуют серьезные, конкурентные причины для большого разнообразия очень сложных ограничений. И они могут сильно выступать против более традиционных и фондовых ответов.
Что касается читабельности, я считаю, что код читабелен, если он написан непротиворечивым образом, который я могу выучить, читая его. И там, где нет преднамеренной попытки запутать код. Там действительно не намного больше требуется.
Читаемый код может быть достаточно эффективным и соответствовать всем вышеупомянутым требованиям, которые я уже упоминал. Главное, что вы полностью понимаете, что каждая строка кода, которую вы пишете, производит на уровне сборки или машины, когда вы ее кодируете. C ++ накладывает серьезное бремя на программиста, потому что во многих ситуациях идентичные фрагменты кода C ++ фактически генерируют разные фрагменты машинного кода, которые имеют совершенно разные характеристики. Но C, как правило, в основном "то, что вы видите, то и получаете". Так что в этом отношении безопаснее.
РЕДАКТИРОВАТЬ за JasonS:
Я использую C с 1978 года, а C ++ примерно с 1987 года, и у меня большой опыт использования как для мейнфреймов, так и для миникомпьютеров и (в основном) встроенных приложений.
Джейсон приводит комментарий об использовании «inline» в качестве модификатора. (С моей точки зрения, это относительно «новая» возможность, потому что она просто не существовала, возможно, половину моей жизни или более с использованием C и C ++.) Использование встроенных функций может фактически делать такие вызовы (даже для одной строки код) довольно практично. И это гораздо лучше, где это возможно, чем использование макроса из-за типизации, которую может применить компилятор.
Но есть и ограничения. Во-первых, вы не можете полагаться на то, что компилятор «поймет подсказку». Может или не может. И есть веские причины, чтобы не понять намек. (Для очевидного примера, если адрес функции взят, для этого требуется создание экземпляра функции, а использование адреса для выполнения вызова ... потребует вызова. Тогда код не может быть встроен.) и другие причины. Компиляторы могут иметь широкий спектр критериев, по которым они судят, как обрабатывать подсказку. И как программист, это означает, что вы должныпотратьте некоторое время на изучение этого аспекта компилятора, иначе вы, скорее всего, примете решение, основываясь на ошибочных идеях. Таким образом, это увеличивает нагрузку как на автора кода, так и на любого читателя, а также на любого, кто планирует перенести код на другой компилятор.
Также компиляторы C и C ++ поддерживают раздельную компиляцию. Это означает, что они могут скомпилировать один фрагмент кода C или C ++ без компиляции любого другого связанного кода для проекта. Для того, чтобы встроить код, предполагая, что компилятор в противном случае может сделать это, он должен не только иметь объявление «в области видимости», но и также иметь определение. Обычно программисты работают, чтобы убедиться, что это так, если они используют «встроенный». Но ошибки легко закрасться.
В целом, хотя я также использую inline там, где считаю нужным, я склонен полагать, что не могу на это полагаться. Если производительность является существенным требованием, и я думаю, что OP уже ясно написал, что при переходе на более «функциональный» маршрут имело место значительное снижение производительности, тогда я, конечно, предпочел бы избегать использования inline как практики кодирования и вместо этого следовал бы немного другому, но полностью согласованному образцу написания кода.
Последнее замечание о том, что «inline» и определения находятся «в области видимости» для отдельного этапа компиляции. Возможно (не всегда надежно) выполнение работ на этапе связывания. Это может произойти в том и только в том случае, если компилятор C / C ++ внедряет в объектные файлы достаточно деталей, чтобы компоновщик мог выполнять «встроенные» запросы. Лично у меня не было системы компоновщика (вне Microsoft), которая бы поддерживала эту возможность. Но это может произойти. Опять же, следует ли полагаться на это или нет, будет зависеть от обстоятельств. Но я обычно предполагаю, что это не было переложено на компоновщик, если я не знаю иначе, основываясь на убедительных доказательствах. И если я на это полагаюсь, это будет задокументировано на видном месте.
C ++
Для тех, кто интересуется, вот пример того, почему я остаюсь достаточно осторожным в отношении C ++ при кодировании встроенных приложений, несмотря на его готовность сегодня. Я выбросить некоторые термины , которые я думаю , что все встроенные программистам C ++ нужно знать холод :
- частичная специализация шаблона
- виртуальные таблицы
- виртуальный базовый объект
- рамка активации
- раскрутка активационной рамки
- использование умных указателей в конструкторах и почему
- оптимизация возвращаемого значения
Это всего лишь короткий список. Если вы еще не знаете все об этих терминах и почему я их перечислил (и многие другие я не перечислил здесь), то я бы посоветовал не использовать C ++ для встроенной работы, если это не вариант для проекта. ,
Давайте кратко рассмотрим семантику исключений в C ++, чтобы получить представление.
AВ
A
.
.
foo ();
String s;
foo ();
.
.
A
В
Компилятор C ++ видит первый вызов foo () и может просто позволить размотке обычного кадра активации произойти, если foo () выдает исключение. Другими словами, компилятор C ++ знает, что в этот момент не требуется никакого дополнительного кода для поддержки процесса раскрутки фрейма, участвующего в обработке исключений.
Но как только String s создан, компилятор C ++ знает, что он должен быть должным образом уничтожен, прежде чем можно будет разрешить раскрутку кадра, если позднее произойдет исключение. Таким образом, второй вызов foo () семантически отличается от первого. Если второй вызов функции foo () генерирует исключение (что он может или не может делать), компилятор должен разместить код, предназначенный для обработки уничтожения String, прежде чем позволить обычному размотке кадра произойти. Это отличается от кода, необходимого для первого вызова foo ().
(Можно добавить дополнительные декорации в C ++, чтобы помочь ограничить эту проблему. Но дело в том, что программисты, использующие C ++, просто должны гораздо лучше понимать последствия каждой строки кода, которую они пишут.)
В отличие от malloc C, новый C ++ использует исключения, чтобы сигнализировать, когда он не может выполнить необработанное выделение памяти. Так будет и «dynamic_cast». (См. 3-е изд. Страуструпа «Язык программирования C ++», стр. 384 и 385, для стандартных исключений в C ++.) Компиляторы могут разрешить отключение этого поведения. Но в целом вы будете подвергаться некоторым накладным расходам из-за правильно сформированных прологов и эпилогов обработки исключений в сгенерированном коде, даже когда исключения на самом деле не имеют места и даже когда скомпилированная функция фактически не имеет каких-либо блоков обработки исключений. (Страуструп публично посетовал на это.)
Без частичной специализации шаблонов (не все компиляторы C ++ поддерживают это) использование шаблонов может привести к катастрофе для встроенного программирования. Без этого расцвет кода будет серьезным риском, который может убить встроенный проект с небольшой памятью в одно мгновение.
Когда функция C ++ возвращает объект, временный компилятор без имени создается и уничтожается. Некоторые компиляторы C ++ могут предоставить эффективный код, если в операторе return используется конструктор объекта вместо локального объекта, что уменьшает потребность в строительстве и разрушении одним объектом. Но не каждый компилятор делает это, и многие программисты на C ++ даже не знают об этой «оптимизации возвращаемого значения».
Предоставление конструктора объекта с одним типом параметра может позволить компилятору C ++ найти путь преобразования между двумя типами совершенно неожиданным для программиста способом. Такое «умное» поведение не является частью C.
Предложение catch, задающее базовый тип, будет «разрезать» брошенный производный объект, потому что брошенный объект копируется с использованием «статического типа» предложения catch, а не «динамического типа» объекта. Весьма распространенный источник страданий от исключений (когда вы чувствуете, что можете даже позволить себе исключения во встроенном коде.)
Компиляторы C ++ могут автоматически генерировать для вас конструкторы, деструкторы, конструкторы копирования и операторы присваивания с непредвиденными результатами. Требуется время, чтобы получить средства с деталями этого.
Передача массивов производных объектов в функцию, принимающую массивы базовых объектов, редко генерирует предупреждения компилятора, но почти всегда приводит к некорректному поведению.
Поскольку C ++ не вызывает деструктор частично сконструированных объектов, когда в конструкторе объектов возникает исключение, обработка исключений в конструкторах обычно требует «умных указателей», чтобы гарантировать, что сконструированные фрагменты в конструкторе будут должным образом уничтожены, если там произойдет исключение. , (См. Страуструп, стр. 367 и 368.) Это распространенная проблема при написании хороших классов на C ++, но, конечно же, ее избегают в C, так как C не имеет встроенной семантики построения и уничтожения. Написание правильного кода для обработки конструкции подобъекты внутри объекта означают написание кода, который должен справиться с этой уникальной семантической проблемой в C ++; Другими словами, «написание вокруг» C ++ семантического поведения.
C ++ может копировать объекты, переданные в параметры объекта. Например, в следующих фрагментах вызов "rA (x);" может заставить компилятор C ++ вызывать конструктор для параметра p, чтобы затем вызвать конструктор копирования для передачи объекта x в параметр p, затем другой конструктор для возвращаемого объекта (неназванного временного) функции rA, которая, конечно, скопировано из параметра p. Хуже того, если у класса А есть свои собственные объекты, которые нуждаются в строительстве, это может привести к катастрофическим телескопам. (Программист AC избежал бы большей части этого мусора, оптимизируя вручную, поскольку программисты на C не имеют такого удобного синтаксиса и должны выражать все детали по одному).
class A {...};
A rA (A p) { return p; }
// .....
{ A x; rA(x); }
Наконец, короткое примечание для программистов на Си. longjmp () не имеет переносимого поведения в C ++. (Некоторые программисты на C используют это как своего рода механизм «исключения».) Некоторые компиляторы C ++ на самом деле пытаются настроить вещи для очистки, когда берется longjmp, но это поведение не переносимо в C ++. Если компилятор очищает построенные объекты, он не переносим. Если компилятор не очищает их, то объекты не уничтожаются, если код покидает область действия созданных объектов в результате longjmp и поведение недопустимо. (Если использование longjmp в foo () не выходит из области видимости, поведение может быть нормальным.) Это не слишком часто используется программистами на C, но они должны знать об этих проблемах перед их использованием.