No se me ocurre ninguna ventaja (pero vea la nota a JasonS en la parte inferior), envolviendo una línea de código como una función o subrutina. Excepto tal vez que pueda nombrar la función algo "legible". Pero también puedes comentar la línea. Y dado que concluir una línea de código en una función cuesta memoria de código, espacio de pila y tiempo de ejecución, me parece que es principalmente contraproducente. En una situación de enseñanza? Puede tener sentido. Pero eso depende de la clase de estudiantes, su preparación previa, el plan de estudios y el maestro. Sobre todo, creo que no es una buena idea. Pero esa es mi opinión.
Lo que nos lleva a la conclusión. Su amplia área de preguntas ha sido, durante décadas, un tema de debate y sigue siendo hoy en día un tema de debate. Entonces, al menos mientras leo su pregunta, me parece una pregunta basada en la opinión (como usted la hizo).
Se podría alejar de ser tan basado en la opinión como es, si fuera más detallado sobre la situación y describiera cuidadosamente los objetivos que tenía como primarios. Cuanto mejor defina sus herramientas de medición, más objetivas serán las respuestas.
En términos generales, desea hacer lo siguiente para cualquier codificación. (Para más adelante, supondré que estamos comparando diferentes enfoques, todos los cuales logran los objetivos. Obviamente, cualquier código que no realice las tareas necesarias es peor que el código que tiene éxito, independientemente de cómo se escriba).
- Sea coherente con su enfoque, de modo que otra lectura de su código pueda desarrollar una comprensión de cómo aborda su proceso de codificación. Ser inconsistente es probablemente el peor crimen posible. No solo hace que sea difícil para los demás, sino que también te dificulta volver al código años después.
- En la medida de lo posible, intente y organice las cosas para que la inicialización de varias secciones funcionales se pueda realizar sin tener en cuenta los pedidos. Donde se requiere ordenar, si se debe a un acoplamiento cercano de dos subfunciones altamente relacionadas, considere una única inicialización para ambas para que pueda reordenarse sin causar daño. Si eso no es posible, documente el requisito de pedido de inicialización.
- Encapsula el conocimiento en exactamente un lugar, si es posible. Las constantes no deben duplicarse en todo el lugar del código. Las ecuaciones que resuelven algunas variables deberían existir en un solo lugar. Y así. Si se encuentra copiando y pegando un conjunto de líneas que realizan un comportamiento necesario en una variedad de ubicaciones, considere una forma de capturar ese conocimiento en un lugar y utilizarlo donde sea necesario. Por ejemplo, si tiene una estructura de árbol que se debe caminar de una manera específica, noRepita el código de caminar por el árbol en todos y cada uno de los lugares donde necesita recorrer los nodos del árbol. En cambio, capture el método de caminar por los árboles en un lugar y úselo. De esta manera, si el árbol cambia y el método de caminar cambia, solo tiene un lugar por el cual preocuparse y todo el resto del código "funciona bien".
- Si extiende todas sus rutinas en una hoja de papel enorme y plana, con flechas que las conectan como las llaman otras rutinas, verá en cualquier aplicación que habrá "grupos" de rutinas que tienen muchas flechas. entre ellos pero solo unas pocas flechas fuera del grupo. Por lo tanto, habrá límites naturales de rutinas estrechamente acopladas y conexiones poco acopladas entre otros grupos de rutinas estrechamente acopladas. Use este hecho para organizar su código en módulos. Esto limitará la complejidad aparente de su código, sustancialmente.
Lo anterior es generalmente cierto sobre toda la codificación. No discutí el uso de parámetros, variables globales locales o estáticas, etc. La razón es que para la programación incrustada, el espacio de la aplicación a menudo impone nuevas restricciones extremas y muy significativas y es imposible discutir todas ellas sin discutir cada aplicación incrustada. Y eso no está sucediendo aquí, de todos modos.
Estas restricciones pueden ser cualquiera (y más) de estas:
- Limitaciones de costos severas que requieren MCU extremadamente primitivas con RAM minúscula y casi sin recuento de pines de E / S. Para estos, se aplican conjuntos de reglas completamente nuevos. Por ejemplo, puede que tenga que escribir en el código de ensamblado porque no hay mucho espacio de código. Puede que tenga que usar SOLAMENTE variables estáticas porque el uso de variables locales es demasiado costoso y requiere mucho tiempo. Es posible que deba evitar el uso excesivo de subrutinas porque (por ejemplo, algunas partes de Microchip PIC) solo hay 4 registros de hardware en los que almacenar direcciones de retorno de subrutinas. Por lo tanto, es posible que deba "aplanar" su código dramáticamente. Etc.
- Limitaciones de potencia severas que requieren código cuidadosamente diseñado para iniciar y apagar la mayoría de la MCU y colocar limitaciones severas en el tiempo de ejecución del código cuando se ejecuta a toda velocidad. Nuevamente, esto puede requerir un poco de codificación de ensamblaje, a veces.
- Requisitos de tiempo severos. Por ejemplo, hay momentos en los que he tenido que asegurarme de que la transmisión de un drenaje abierto 0 tuviera que tomar EXACTAMENTE el mismo número de ciclos que la transmisión de un 1. Y que el muestreo de esta misma línea también tuviera que realizarse con una fase relativa exacta a este momento. Esto significaba que C NO podía usarse aquí. La ÚNICA manera posible de hacer esa garantía es elaborar cuidadosamente el código de ensamblaje. (E incluso entonces, no siempre en todos los diseños de ALU).
Y así. (El código de cableado para instrumentación médica vital también tiene un mundo entero propio).
El resultado aquí es que la codificación incrustada a menudo no es algo gratuito, donde puede codificar como lo haría en una estación de trabajo. A menudo hay razones severas y competitivas para una amplia variedad de restricciones muy difíciles. Y estos pueden argumentar fuertemente en contra de las respuestas más tradicionales y comunes .
Con respecto a la legibilidad, encuentro que el código es legible si está escrito de una manera consistente que pueda aprender mientras lo leo. Y donde no hay un intento deliberado de ofuscar el código. Realmente no hay mucho más requerido.
El código legible puede ser bastante eficiente y puede cumplir con todos los requisitos anteriores que ya he mencionado. Lo principal es que comprenda completamente lo que produce cada línea de código que escribe a nivel de ensamblaje o máquina, a medida que lo codifica. C ++ pone una carga seria para el programador aquí porque hay muchas situaciones en las que fragmentos idénticos de código C ++ realmente generan fragmentos diferentes de código de máquina que tienen rendimientos muy diferentes. Pero C, en general, es principalmente un lenguaje de "lo que ves es lo que obtienes". Entonces es más seguro en ese sentido.
EDITAR por JasonS:
He estado usando C desde 1978 y C ++ desde aproximadamente 1987 y he tenido mucha experiencia usando ambos para mainframes, minicomputadoras y (principalmente) aplicaciones integradas.
Jason saca un comentario sobre el uso de 'en línea' como modificador. (En mi perspectiva, esta es una capacidad relativamente "nueva" porque simplemente no existió durante quizás la mitad de mi vida o más usando C y C ++.) El uso de funciones en línea puede hacer tales llamadas (incluso para una línea de código) bastante práctico. Y es mucho mejor, siempre que sea posible, que usar una macro debido a la tipificación que el compilador puede aplicar.
Pero también hay limitaciones. La primera es que no puede confiar en el compilador para "tomar la pista". Puede o no puede. Y hay buenas razones para no entender la indirecta. (Para un ejemplo obvio, si se toma la dirección de la función, esto requiere la creación de instancias de la función y el uso de la dirección para hacer la llamada ... requerirá una llamada. El código no se puede insertar en línea entonces). otras razones también. Los compiladores pueden tener una amplia variedad de criterios por los cuales juzgan cómo manejar la pista. Y como programador, esto significa que debesDedique un tiempo a aprender sobre ese aspecto del compilador o, de lo contrario, es probable que tome decisiones basadas en ideas erróneas. Por lo tanto, agrega una carga tanto al escritor del código como a cualquier lector y también a cualquiera que planee portar el código a otro compilador también.
Además, los compiladores C y C ++ admiten compilación separada. Esto significa que pueden compilar una pieza de código C o C ++ sin compilar ningún otro código relacionado para el proyecto. Para insertar código en línea, suponiendo que el compilador de otra manera pudiera elegir hacerlo, no solo debe tener la declaración "en alcance" sino que también debe tener la definición. Por lo general, los programadores trabajarán para asegurarse de que este sea el caso si están usando 'en línea'. Pero es fácil que los errores se cuelen.
En general, aunque también uso inline donde creo que es apropiado, tiendo a suponer que no puedo confiar en él. Si el rendimiento es un requisito importante, y creo que el OP ya ha escrito claramente que ha habido un impacto significativo en el rendimiento cuando se dirigieron a una ruta más "funcional", entonces ciertamente elegiría evitar confiar en línea como práctica de codificación y en su lugar, seguiría un patrón de escritura de código ligeramente diferente, pero completamente coherente.
Una nota final sobre 'en línea' y las definiciones están "dentro del alcance" para un paso de compilación separado. Es posible (no siempre confiable) que el trabajo se realice en la etapa de vinculación. Esto puede ocurrir si y solo si un compilador de C / C ++ oculta suficientes detalles en los archivos de objeto para permitir que un vinculador actúe sobre solicitudes 'en línea'. Personalmente, no he experimentado un sistema de enlace (fuera de Microsoft) que admita esta capacidad. Pero puede ocurrir. Una vez más, si se debe confiar o no dependerá de las circunstancias. Pero generalmente asumo que esto no se ha incluido en el enlazador, a menos que sepa lo contrario en base a buena evidencia. Y si confío en ello, se documentará en un lugar destacado.
C ++
Para aquellos interesados, aquí hay un ejemplo de por qué sigo siendo bastante cauteloso con C ++ al codificar aplicaciones integradas, a pesar de su disponibilidad actual. Lanzaré algunos términos que creo que todos los programadores de C ++ integrados deben saber en frío :
- especialización parcial de plantilla
- vtables
- objeto base virtual
- marco de activación
- marco de activación desenrollar
- uso de punteros inteligentes en constructores, y por qué
- optimización del valor de retorno
Eso es solo una lista corta. Si aún no sabe todo acerca de esos términos y por qué los enumeré (y muchos más que no enumeré aquí), le aconsejaría que no use C ++ para el trabajo incorporado, a menos que no sea una opción para el proyecto .
Echemos un vistazo rápido a la semántica de excepciones de C ++ para obtener solo un sabor.
UNsi separada, compilada por separado y en un momento diferente.
UN
.
.
foo ();
String s;
foo ();
.
.
UN
si
El compilador de C ++ ve la primera llamada a foo () y solo puede permitir que se produzca un desenlace de activación normal, si foo () arroja una excepción. En otras palabras, el compilador de C ++ sabe que no se necesita ningún código adicional en este punto para admitir el proceso de desenrollado de trama involucrado en el manejo de excepciones.
Pero una vez que se ha creado String s, el compilador de C ++ sabe que debe destruirse correctamente antes de que se pueda permitir un desenrollado de trama, si se produce una excepción más adelante. Entonces, la segunda llamada a foo () es semánticamente diferente de la primera. Si la segunda llamada a foo () arroja una excepción (que puede o no puede hacer), el compilador debe haber colocado un código diseñado para manejar la destrucción de String s antes de permitir que se produzca el desenrollado habitual del marco. Esto es diferente al código requerido para la primera llamada a foo ().
(Es posible agregar decoraciones adicionales en C ++ para ayudar a limitar este problema. Pero el hecho es que los programadores que usan C ++ simplemente deben ser mucho más conscientes de las implicaciones de cada línea de código que escriben).
A diferencia de Malloc de C, el nuevo C ++ usa excepciones para señalar cuando no puede realizar la asignación de memoria sin procesar. También lo hará 'dynamic_cast'. (Consulte la tercera edición de Stroustrup, The C ++ Programming Language, páginas 384 y 385 para ver las excepciones estándar en C ++.) Los compiladores pueden permitir que este comportamiento se deshabilite. Pero, en general, incurrirá en una sobrecarga debido a los prólogos y epílogos de manejo de excepciones correctamente formados en el código generado, incluso cuando las excepciones realmente no tienen lugar e incluso cuando la función que se está compilando en realidad no tiene ningún bloque de manejo de excepciones. (Stroustrup lo ha lamentado públicamente).
Sin una especialización parcial de la plantilla (no todos los compiladores de C ++ lo admiten), el uso de plantillas puede significar un desastre para la programación integrada. Sin él, la floración de código es un riesgo grave que podría matar un proyecto incrustado de memoria pequeña en un instante.
Cuando una función C ++ devuelve un objeto, se crea y destruye un compilador temporal sin nombre. Algunos compiladores de C ++ pueden proporcionar un código eficiente si se usa un constructor de objetos en la declaración de retorno, en lugar de un objeto local, reduciendo las necesidades de construcción y destrucción en un objeto. Pero no todos los compiladores hacen esto y muchos programadores de C ++ ni siquiera son conscientes de esta "optimización del valor de retorno".
Proporcionar un constructor de objetos con un tipo de parámetro único puede permitir que el compilador de C ++ encuentre una ruta de conversión entre dos tipos de maneras completamente inesperadas para el programador. Este tipo de comportamiento "inteligente" no es parte de C.
Una cláusula catch que especifique un tipo base "dividirá" un objeto derivado lanzado, porque el objeto lanzado se copia utilizando el "tipo estático" de la cláusula catch y no el "tipo dinámico" del objeto. Una fuente no común de miseria de excepción (cuando siente que incluso puede permitirse excepciones en su código incrustado).
Los compiladores de C ++ pueden generar automáticamente constructores, destructores, constructores de copias y operadores de asignación para usted, con resultados no deseados. Lleva tiempo ganar facilidad con los detalles de esto.
Pasar matrices de objetos derivados a una función que acepta matrices de objetos base, rara vez genera advertencias del compilador, pero casi siempre produce un comportamiento incorrecto.
Dado que C ++ no invoca el destructor de objetos parcialmente construidos cuando se produce una excepción en el constructor de objetos, el manejo de excepciones en los constructores generalmente exige "punteros inteligentes" para garantizar que los fragmentos construidos en el constructor se destruyan adecuadamente si se produce una excepción allí . (Consulte Stroustrup, página 367 y 368.) Este es un problema común al escribir buenas clases en C ++, pero, por supuesto, se evita en C ya que C no tiene la semántica de construcción y destrucción incorporada. Escribir el código adecuado para manejar la construcción de subobjetos dentro de un objeto significa escribir código que debe hacer frente a este problema semántico único en C ++; en otras palabras, "escribir alrededor" de los comportamientos semánticos de C ++.
C ++ puede copiar objetos pasados a parámetros de objeto. Por ejemplo, en los siguientes fragmentos, la llamada "rA (x);" puede hacer que el compilador de C ++ invoque un constructor para el parámetro p, para luego llamar al constructor de copia para transferir el objeto x al parámetro p, y luego otro constructor para el objeto de retorno (un temporario sin nombre) de la función rA, que por supuesto es copiado del parámetro p. Peor aún, si la clase A tiene sus propios objetos que necesitan construcción, esto puede telescopizar desastrosamente. (El programador de AC evitaría la mayor parte de esta basura, optimizando a mano ya que los programadores de C no tienen una sintaxis tan práctica y tienen que expresar todos los detalles uno por uno).
class A {...};
A rA (A p) { return p; }
// .....
{ A x; rA(x); }
Finalmente, una breve nota para los programadores de C. longjmp () no tiene un comportamiento portátil en C ++. (Algunos programadores de C usan esto como una especie de mecanismo de "excepción".) Algunos compiladores de C ++ en realidad intentarán configurar las cosas para limpiar cuando se toma el longjmp, pero ese comportamiento no es portátil en C ++. Si el compilador limpia los objetos construidos, no es portátil. Si el compilador no los limpia, entonces los objetos no se destruyen si el código abandona el alcance de los objetos construidos como resultado de longjmp y el comportamiento no es válido. (Si el uso de longjmp en foo () no deja un alcance, entonces el comportamiento puede estar bien). Esto no es usado con demasiada frecuencia por los programadores incrustados en C, pero deben ser conscientes de estos problemas antes de usarlos.