Não há vantagem em que eu possa pensar (mas veja a nota do JasonS na parte inferior), agrupando uma linha de código como uma função ou sub-rotina. Exceto, talvez, que você possa nomear a função como "legível". Mas você também pode comentar a linha. E desde que agrupar uma linha de código em uma função custa memória de código, espaço de pilha e tempo de execução, parece-me que é principalmente contraproducente. Em uma situação de ensino? Isso pode fazer algum sentido. Mas isso depende da classe dos alunos, de sua preparação prévia, do currículo e do professor. Principalmente, acho que não é uma boa ideia. Mas essa é a minha opinião.
O que nos leva à linha de fundo. Sua ampla área de perguntas tem sido, há décadas, uma questão de algum debate e permanece até hoje uma questão de algum debate. Então, pelo menos enquanto eu leio sua pergunta, parece-me uma pergunta baseada em opinião (como você fez).
Poderia deixar de ser tão baseado em opiniões quanto fosse, se você fosse mais detalhado sobre a situação e descrevesse cuidadosamente os objetivos que considerava primários. Quanto melhor você definir suas ferramentas de medição, mais objetivas serão as respostas.
Em termos gerais, você deseja fazer o seguinte para qualquer codificação. (Abaixo, assumirei que estamos comparando abordagens diferentes, todas as quais atingem os objetivos. Obviamente, qualquer código que falha ao executar as tarefas necessárias é pior do que o código que obtém êxito, independentemente de como ele seja escrito.)
- Seja consistente com sua abordagem, para que outra pessoa que esteja lendo seu código possa entender como você aborda seu processo de codificação. Ser inconsistente é provavelmente o pior crime possível. Isso não apenas torna difícil para os outros, mas também para você voltar ao código anos depois.
- Na medida do possível, tente organizar as coisas para que a inicialização de várias seções funcionais possa ser realizada sem levar em consideração pedidos. Onde a encomenda é necessária, se for devido a um acoplamento próximo de duas subfunções altamente relacionadas, considere uma inicialização única para ambas, para que possa ser reordenada sem causar danos. Se isso não for possível, documente o requisito de pedido de inicialização.
- O conhecimento encapsulado em exatamente um lugar, se possível. As constantes não devem ser duplicadas em todo o lugar no código. Equações que resolvem alguma variável devem existir em um e apenas um lugar. E assim por diante. Se você copiar e colar um conjunto de linhas que executam algum comportamento necessário em vários locais, considere uma maneira de capturar esse conhecimento em um local e usá-lo onde necessário. Por exemplo, se você tem uma estrutura em árvore que deve ser percorrida de uma maneira específica, nãoreplique o código de caminhada na árvore em todos os lugares em que você precisa percorrer os nós da árvore. Em vez disso, capture o método de caminhar em árvores em um só lugar e use-o. Dessa forma, se a árvore mudar e o método andando mudar, você terá apenas um lugar para se preocupar e todo o restante do código "funcionará corretamente".
- Se você espalhar todas as suas rotinas em uma folha de papel enorme e plana, com setas conectando-as como são chamadas por outras rotinas, você verá em qualquer aplicativo que haverá "grupos" de rotinas que têm muitas e muitas flechas entre si, mas apenas algumas flechas fora do grupo. Portanto, haverá limites naturais de rotinas estreitamente acopladas e conexões pouco acopladas entre outros grupos de rotinas estreitamente acopladas. Use esse fato para organizar seu código em módulos. Isso limitará a aparente complexidade do seu código, substancialmente.
O acima descrito é geralmente verdade sobre toda a codificação. Não discuti o uso de parâmetros, variáveis globais locais ou estáticas, etc. O motivo é que, para a programação incorporada, o espaço do aplicativo geralmente coloca novas restrições extremas e muito significativas e é impossível discutir todos eles sem discutir todos os aplicativos incorporados. E isso não está acontecendo aqui, de qualquer maneira.
Essas restrições podem ser qualquer uma (e mais) delas:
- Limitações de custo severas que exigem MCUs extremamente primitivas com RAM minúscula e quase nenhuma contagem de pinos de E / S. Para esses, novos conjuntos de regras se aplicam. Por exemplo, talvez você precise escrever no código do assembly porque não há muito espaço no código. Talvez você precise usar SOMENTE variáveis estáticas porque o uso de variáveis locais é muito caro e consome tempo. Pode ser necessário evitar o uso excessivo de sub-rotinas porque (por exemplo, algumas peças Microchip PIC), existem apenas 4 registros de hardware nos quais os endereços de retorno da sub-rotina são armazenados. Portanto, talvez você precise "achatar" dramaticamente seu código. Etc.
- Limitações severas de energia que exigem código cuidadosamente criado para inicializar e desligar a maior parte do MCU e impõem severas limitações ao tempo de execução do código ao executar em velocidade máxima. Novamente, isso pode exigir alguma codificação de montagem, às vezes.
- Requisitos de tempo severos. Por exemplo, há momentos em que tive que me certificar de que a transmissão de um dreno aberto 0 levasse EXATAMENTE o mesmo número de ciclos que a transmissão de um 1. E que a amostragem dessa mesma linha também deveria ser realizada com uma fase relativa exata para esse momento. Isso significava que C NÃO poderia ser usado aqui. A única maneira possível de fazer essa garantia é criar cuidadosamente o código de montagem. (E mesmo assim, nem sempre em todos os designs da ALU.)
E assim por diante. (O código de fiação para instrumentação médica essencial à vida também possui um mundo inteiro.)
O resultado aqui é que a codificação incorporada geralmente não é gratuita para todos, onde você pode codificar como faria em uma estação de trabalho. Muitas vezes, existem razões competitivas graves para uma grande variedade de restrições muito difíceis. E eles podem argumentar fortemente contra as respostas mais tradicionais e de ações .
Em relação à legibilidade, acho que o código é legível se for escrito de uma maneira consistente que eu possa aprender enquanto o leio. E onde não há uma tentativa deliberada de ofuscar o código. Realmente não é muito mais necessário.
O código legível pode ser bastante eficiente e pode atender a todos os requisitos acima mencionados. O principal é que você entenda completamente o que cada linha de código que você escreve produz no nível da montagem ou da máquina, conforme você a codifica. O C ++ coloca uma carga séria no programador aqui, porque há muitas situações em que trechos idênticos de código C ++ realmente geram trechos diferentes de código de máquina com desempenhos muito diferentes. Mas C, geralmente, é principalmente uma linguagem "o que você vê é o que recebe". Portanto, é mais seguro nesse sentido.
EDIT por JasonS:
Uso C desde 1978 e C ++ desde 1987 e tenho muita experiência usando ambos para mainframes, minicomputadores e (principalmente) aplicativos incorporados.
Jason traz um comentário sobre o uso de 'inline' como um modificador. (Na minha perspectiva, esse é um recurso relativamente "novo", porque ele simplesmente não existiu por talvez metade da minha vida ou mais usando C e C ++.) O uso de funções em linha pode realmente fazer essas chamadas (mesmo para uma linha de código) bastante prático. E é muito melhor, sempre que possível, do que usar uma macro devido à digitação que o compilador pode aplicar.
Mas existem limitações também. A primeira é que você não pode confiar no compilador para "entender a dica". Pode ser que sim ou que não. E há boas razões para não dar a dica. (Para um exemplo óbvio, se o endereço da função for utilizado, isso requer a instanciação da função e o uso do endereço para fazer a chamada exigirá ... uma chamada. O código não pode ser incorporado então.) outras razões também. Os compiladores podem ter uma ampla variedade de critérios pelos quais julgam como lidar com a dica. E como programador, isso significa que você devegaste algum tempo aprendendo sobre esse aspecto do compilador; caso contrário, é provável que você tome decisões com base em idéias defeituosas. Portanto, isso adiciona um ônus ao escritor do código e também a qualquer leitor e também a quem planeja portar o código para outro compilador.
Além disso, os compiladores C e C ++ oferecem suporte à compilação separada. Isso significa que eles podem compilar uma parte do código C ou C ++ sem compilar nenhum outro código relacionado para o projeto. Para codificar em linha, supondo que o compilador possa optar por fazê-lo, ele não apenas deve ter a declaração "no escopo", mas também deve ter a definição. Geralmente, os programadores trabalharão para garantir que este seja o caso se eles estiverem usando 'inline'. Mas é fácil os erros aparecerem.
Em geral, embora eu também use inline onde achar apropriado, eu suponho que não posso confiar nela. Se o desempenho é um requisito significativo, e acho que o OP já escreveu claramente que houve um impacto significativo no desempenho quando foram para uma rota mais "funcional", então eu certamente escolheria evitar confiar na linha como prática de codificação e em vez disso, seguiria um padrão ligeiramente diferente, mas inteiramente consistente, de escrever código.
Uma observação final sobre 'inline' e definições sendo "dentro do escopo" para uma etapa de compilação separada. É possível (nem sempre confiável) que o trabalho seja realizado no estágio de vinculação. Isso pode ocorrer se, e somente se, um compilador C / C ++ enterrar detalhes suficientes nos arquivos de objeto para permitir que um vinculador atue em solicitações "em linha". Pessoalmente, não experimentei um sistema de vinculação (fora da Microsoft) que suporta esse recurso. Mas isso pode ocorrer. Novamente, se deve ou não confiar, dependerá das circunstâncias. Mas geralmente presumo que isso não tenha sido colocado no linker, a menos que eu saiba de outra forma com base em boas evidências. E se eu confiar nisso, será documentado em um lugar de destaque.
C ++
Para os interessados, aqui está um exemplo de por que permaneço bastante cauteloso em C ++ ao codificar aplicativos incorporados, apesar de sua pronta disponibilidade hoje. Vou descartar alguns termos que acho que todos os programadores C ++ incorporados precisam conhecer a frio :
- especialização parcial do modelo
- vtables
- objeto base virtual
- quadro de ativação
- ativação do quadro de ativação
- uso de ponteiros inteligentes em construtores e por que
- otimização do valor de retorno
Essa é apenas uma pequena lista. Se você ainda não sabe tudo sobre esses termos e por que os listei (e muitos mais não listei aqui), desaconselho o uso de C ++ para trabalho incorporado, a menos que não seja uma opção para o projeto .
Vamos dar uma olhada rápida na semântica de exceção do C ++ para obter apenas uma amostra.
UMAB
UMA
.
.
foo ();
String s;
foo ();
.
.
A
B
O compilador C ++ vê a primeira chamada para foo () e pode apenas permitir que um quadro de ativação normal ocorra, se foo () lançar uma exceção. Em outras palavras, o compilador C ++ sabe que nenhum código extra é necessário neste momento para dar suporte ao processo de desenrolamento de quadros envolvido no tratamento de exceções.
Porém, depois que a String s é criada, o compilador C ++ sabe que deve ser destruído adequadamente antes que um desenrolamento de quadros possa ser permitido, se uma exceção ocorrer posteriormente. Portanto, a segunda chamada para foo () é semanticamente diferente da primeira. Se a segunda chamada para foo () gerar uma exceção (o que pode ou não ser feito), o compilador deve ter colocado um código projetado para lidar com a destruição de String s antes de permitir que o quadro normal ocorra. Isso é diferente do código necessário para a primeira chamada para foo ().
(É possível adicionar decorações adicionais em C ++ para ajudar a limitar esse problema. Mas o fato é que os programadores que usam C ++ precisam estar muito mais cientes das implicações de cada linha de código que escrevem.)
Diferentemente do malloc de C, o novo C ++ usa exceções para sinalizar quando não é possível executar a alocação de memória bruta. Então será 'dynamic_cast'. (Consulte a 3ª edição da Stroustrup, The C ++ Programming Language, páginas 384 e 385 para obter as exceções padrão em C ++.) Os compiladores podem permitir que esse comportamento seja desabilitado. Mas, em geral, você terá uma sobrecarga devido a prólogos e epílogos de manipulação de exceção adequadamente formados no código gerado, mesmo quando as exceções realmente não ocorrerem e mesmo quando a função que está sendo compilada não tiver realmente nenhum bloco de manipulação de exceção. (Stroustrup lamentou isso publicamente).
Sem a especialização parcial de modelos (nem todos os compiladores C ++ oferecem suporte), o uso de modelos pode significar desastre para a programação incorporada. Sem ele, o código bloom é um risco sério que pode matar um projeto incorporado de memória pequena rapidamente.
Quando uma função C ++ retorna um objeto, um compilador temporário sem nome é criado e destruído. Alguns compiladores C ++ podem fornecer código eficiente se um construtor de objetos for usado na instrução de retorno, em vez de um objeto local, reduzindo as necessidades de construção e destruição de um objeto. Mas nem todo compilador faz isso e muitos programadores de C ++ nem sequer estão cientes dessa "otimização do valor de retorno".
Fornecer um construtor de objeto com um único tipo de parâmetro pode permitir que o compilador C ++ encontre um caminho de conversão entre dois tipos de maneiras completamente inesperadas para o programador. Esse tipo de comportamento "inteligente" não faz parte do C.
Uma cláusula catch que especifica um tipo de base "fatiará" um objeto derivado lançado, porque o objeto lançado é copiado usando o "tipo estático" da cláusula catch e não o "tipo dinâmico" do objeto. Uma fonte incomum de miséria de exceção (quando você sente que pode pagar exceções no seu código incorporado).
Os compiladores C ++ podem gerar automaticamente construtores, destruidores, construtores de cópia e operadores de atribuição para você, com resultados indesejados. Leva tempo para obter facilidade com os detalhes disso.
Passar matrizes de objetos derivados para uma função que aceita matrizes de objetos base, raramente gera avisos do compilador, mas quase sempre gera comportamento incorreto.
Como o C ++ não invoca o destruidor de objetos parcialmente construídos quando ocorre uma exceção no construtor de objetos, o tratamento de exceções nos construtores geralmente exige "ponteiros inteligentes" para garantir que fragmentos construídos no construtor sejam destruídos corretamente se ocorrer uma exceção no local. . (Consulte Stroustrup, páginas 367 e 368.) Esse é um problema comum ao escrever boas classes em C ++, mas é claro evitado em C, pois C não possui a semântica de construção e destruição incorporada. Escrevendo código adequado para lidar com a construção de subobjetos em um objeto significa escrever código que deve lidar com esse problema semântico exclusivo em C ++; em outras palavras, "escrevendo em torno" de comportamentos semânticos do C ++.
C ++ pode copiar objetos passados para parâmetros de objeto. Por exemplo, nos seguintes fragmentos, a chamada "rA (x);" pode fazer com que o compilador C ++ chame um construtor para o parâmetro p, a fim de chamar o construtor de cópia para transferir o objeto x para o parâmetro p, então outro construtor para o objeto de retorno (um temporário sem nome) da função rA, que obviamente é copiado do parâmetro p. Pior ainda, se a classe A tiver seus próprios objetos que precisam de construção, isso pode ser causado por um telescópio desastroso. (O programador CA evitaria a maior parte desse lixo, otimizando manualmente, pois os programadores C não possuem uma sintaxe tão prática e precisam expressar todos os detalhes, um de cada vez.)
class A {...};
A rA (A p) { return p; }
// .....
{ A x; rA(x); }
Finalmente, uma breve nota para programadores C. longjmp () não possui um comportamento portátil em C ++. (Alguns programadores de C usam isso como um tipo de mecanismo de "exceção".) Alguns compiladores de C ++ realmente tentam configurar as coisas para limpar quando o longjmp é obtido, mas esse comportamento não é portátil no C ++. Se o compilador limpar objetos construídos, ele não será portátil. Se o compilador não os limpar, os objetos não serão destruídos se o código deixar o escopo dos objetos construídos como resultado do longjmp e o comportamento for inválido. (Se o uso de longjmp em foo () não deixar um escopo, o comportamento poderá ser bom.) Isso não é muito usado pelos programadores incorporados em C, mas eles devem se conscientizar desses problemas antes de usá-los.