Code Craft - Insertar C ++: Plantillas

El lenguaje C ++ es genial. No hay duda de ello. Una razón por la que C ++ es excelente es para permitir flexibilidad en la técnica utilizada para resolver un problema. Si tiene un sistema realmente pequeño, puede ceñirse al código de procedimiento encapsulado por clases. Un proyecto con algunas entidades similares pero ligeramente diferentes podría tratarse mejor con herencia y polimorfismo.

Se utiliza una tercera técnica no notificado, que se implementan en C ++ usando patrones. Las plantillas tienen algunas similitudes con # definir macros pero son mucho más seguras. El compilador no ve el código ingresado por una macro solo después de haberlo ingresado en la fuente. Si el código es incorrecto, los mensajes de error pueden ser muy confusos, porque todo lo que ve el programador es el nombre de la macro. Una plantilla busca errores de sintaxis básicos del compilador cuando se ve por primera vez y nuevamente más tarde cuando se crea el código. Este primer paso elimina mucha confusión, ya que aparecen mensajes de error en el sitio del problema.

Los patrones también son mucho más poderosos. De hecho, son un lenguaje completo de Turing. Se escribieron programas enteros sin sentido utilizando plantillas. Toda la tarea de resultado alcanzable es informar los resultados mediante todos los cálculos realizados por el compilador. No se preocupe, no vamos allí en este artículo.

Plantillas básicas

Puede utilizar plantillas para crear funciones y clases. La forma de hacer esto es bastante similar para ambos, así que comencemos con una función de plantilla de ejemplo:

template<typename T, int EXP = 2>
T power(const T value, int exp = EXP) {
	T res { value };
	for (; exp > 1; --exp) {
		res *= value;
	}
	return res;
}

Esta es una función de plantilla para aumentar valor por el exponente entero, Exp. La palabra clave patrón va seguido entre paréntesis angulares por parámetros. Un parámetro se especifica usando ambos escribe un nombre o clase seguido de un nombre o de un tipo de datos entero seguido de un nombre. También puede usar una función o clase como parámetro de plantilla, pero no veremos ese uso.

El nombre del parámetro se usa en el cuerpo de la clase o función del mismo modo que usaría cualquier otro tipo o valor. Aquí usamos T como el nombre típico para los valores de entrada y retorno de la función. El entero EXP se usa para establecer un valor predeterminado de 2 para el exponente, así que haz potencia para calcular el cuadrado.

Cuando el compilador crea una función o clase de plantilla, crea el mismo código que una versión escrita a mano. Los tipos de datos y valores se ingresan como reemplazos de texto. Esto crea una nueva versión escrita a partir de los argumentos reales a los parámetros. Cada conjunto diverso de argumentos crea una nueva función o tipo. Por ejemplo, un caso de energía () para enteros no es lo mismo que energía () para flotadores. De manera similar, como veremos en un momento, la clase Triple para enteros no es lo mismo que uno para flotante. Cada uno es de un tipo distinto con un código distinto.

Ekde energía () es una función de plantilla que funcionará directamente para cualquier tipo de datos digitales, enteros o comas. Pero si desea utilizarlo con un tipo más complejo como el Triple clase del último artículo? Veamos.

Usar plantillas

Aquí está la declaración de Triple reducido a lo que se necesita para este artículo:

class Triple {
public:
	Triple(const int x, const int y, const int z);
	Triple& operator *=(const Triple& rhs);

	int x() const;
	int y() const;
	int z() const;

private:
	int mX { 0 };	// c++11 member initialization
	int mY { 0 };
	int mZ { 0 };
};

Cambié el más igual operador al numeroso igual operador porque es requerido por el energía() función.

Así es como energía() La función se usa para un número entero, un flotante y el nuestro. Triple tipos de datos:

int p = power(2, 3);
float f = power(4.1, 2);

Triple t(2, 3, 4);
Triple res = power(t, 3);

El único requisito para utilizar un tipo de datos de definición usado (UDT) como Triple con energía() si se debe definir el UDT operador = *() función miembro.

Clases de plantillas

Suponga que ha estado utilizando Triple en un proyecto durante algún tiempo con valores enteros. Ahora la demanda del proyecto lo necesita para valores de coma flotante. El código de clase triple está depurado y es funcional, y es más complejo que lo que vimos aquí. No es agradable pensar en crear una nueva clase para un flotador. También hay indicios de que podría ser necesaria una versión larga o doble.

Con poco trabajo, Triple se puede convertir a una versión general como clase de plantilla. En realidad, es bastante simple. Empiece con el patrón declaración como con la función energía() y reemplazar todas las declaraciones de En t con T. También verifique los argumentos de la función miembro para pasar parámetros por valor. Es posible que deban cambiarse a referencias para manejar de manera más eficiente tipos de datos o UDT más grandes. Cambié los parámetros de compilación a referencias por ese motivo.

Aquí está Triple como una clase de plantilla:

template<typename T>
class Triple {
public:
	Triple(const T& x, const T& y, const T& z);
	Triple& operator *=(const Triple& rhs);

	T x() const;
	T y() const;
	T z() const;

private:
	T mX { 0 };	// c++11 member initialization
	T mY { 0 };
	T mZ { 0 };
};

No mucha diferencia. Así es como podría usarse:

Triple<int> ires = power(Triple { 2, 3, 4 }, 3);
Triple fres = power(Triple(1.2F, 2.2, 3.3)); // calc square
Triple dres = power(Triple(1.2, 2.2, 3.3));// calc square
Triple lres = power(Triple(1, 2, 3.3), 2);

Desafortunadamente, la nueva flexibilidad cuesta decirle a la plantilla el tipo de datos que se debe usar para Triple. Esto se hace colocando el tipo de datos entre paréntesis después del nombre de la clase. Si eso es una molestia, siempre puede usarlo typedef o el nuevo usando crear alias:

using TripleInt = Triple;
TripleInt ires = power(Triple { 2, 3, 4 }, 3);

Por lo tanto, la creación de una clase de plantilla ahorra costos de depuración y mantenimiento en general. Una vez que el código funciona, funciona para todos los tipos de datos relacionados. Si se encuentra y se corrige un error, se corrige para todas las versiones.

Tiempo de plantilla y tamaño de código

El código generado por una plantilla es exactamente el mismo código que una versión manuscrita de la misma función o clase. Todo lo que cambia entre versiones es el tipo de datos utilizado en la instancia. Debido a que el código es el mismo que el de la versión manuscrita, el tiempo será el mismo. Por lo tanto, no es necesario probar la sincronización. ¡No!

Las plantillas son código de Internet

Las plantillas son básicamente código en línea. Esto significa que cada vez que utiliza una función de plantilla o una función de miembro de clase de plantilla, el código se duplica en línea. Cada caso con un tipo de datos diferente crea su propia codificación, pero eso no será más que si escribiera una clase para cada tipo de datos. Se puede guardar usando clases de plantilla, porque las funciones miembro no se crean si no se usan. Por ejemplo, si el Triple funciones de clase getter - x (), y (), z () - nunca se utilizan, su código no se crea una instancia. Serían para una clase normal, aunque un enlace inteligente podría eliminarlos de lo factible.

Considere el siguiente uso de energía () y Triple:

int i1 = power(2, 3);
int i2 = power(3, 3);
Triple t1 = power(Triple(1, 2, 3), 2);

Esto crea dos versiones enteras en línea de power, aunque ambas se crean para el mismo tipo de datos. Se crea otro caso para la versión Triple. Una sola copia del Triple La clase se crea porque el tipo de datos siempre es un int.

Aquí confiamos defecto instanciación. Esto significa que dejamos que el compilador determine cuándo y dónde se genera el código. Tambien es explícito instanciación, que permite al programador especificar dónde se produce el código. Esto requiere un poco de esfuerzo y saber qué tipos de datos se utilizan para las plantillas.

En general, la instanciación implícita significa código funcional en línea con la opción de duplicar código. Si eso importa depende de la función. Cuando se llama a una función, no a una función en línea, hay un superior en la llamada. Los parámetros de la función se insertan en la pila junto con la información operativa. Cuando la función regresa, esas operaciones se invierten. Para una función pequeña, la llamada puede tomar más código que el cuerpo de la función. En ese caso, la función en línea es más eficaz.

La energía () una función usada aquí es interesante porque el código de la función y el código para invocarla a Uno son similares. Por supuesto, ambos varían según el tipo de datos porque los tipos de datos grandes requieren más manipulación de la pila. En un Arduino One, llamando pagower () con int toma más código que la función. Para un flotador, la llamada es un poco mayor. Para Triple, el código invocado es un poco más grande. Sobre otros procesadores la llamada energía () podría ser diferente. Recuerda eso energía () es una función realmente pequeña. Funciones más grandes, especialmente funciones de miembros, generalmente excede el costo para nombrarlos.

Indique dónde el compilador genera el código. explícito instanciación. Esto forzará una llamada fuera de línea con la sobrecarga asociada. En un archivo fuente, le dice al compilador qué especiales necesita. Para el escenario de prueba los queremos para int y Triple:

template int power(int, int);
template Triple<int> power(Triple<int>, int);

El compilador los creará en el archivo fuente. Luego, como con cualquier otra función, debe crear externo declaración. Esto le dice al compilador que no los cree como en línea. Estas declaraciones son las mismas que las anteriores, solo con externo agregado:

extern template int power(int, int);
extern template Triple power(Triple, int);

Escenario para la prueba del tamaño del código

Algo que necesitaba para crear un escenario de prueba para demostrar las diferencias de tamaño de código entre estas dos instancias. El problema es el resultado de la energía () La función debe usarse más adelante en el código o el compilador optimiza la llamada. Agregar código para usar la función cambia el tamaño general del código de formas que no están relacionadas con el tipo de instante. Esto dificulta las comparaciones comprensibles.

Finalmente decidí crear una clase, Solicitud, con miembros de datos inicializados por el energía () función. Agregar miembros de datos del mismo tipo o de tipos diferentes provoca cambios mínimos en el tamaño total, por lo que el tamaño total del código del programa refleja fielmente los cambios solo debido al tipo de instante.

Aquí está la declaración de Solicitud:

struct Application {
public:
	Application(const int value, const int exp);

	static void loop() {
	}

	int i1;
	int i2;
	int i3;
	Triple t1;
	Triple t2;
	Triple t3;
};

y la implementación del constructor:

Application::Application(int value, const int exp) :
		i1 { power(value++, exp) }, //
				i2 { power<int, 3="">(value++) }, // calcs cube
				i3 { power(value++, exp) }, //

				t1 { power(Triple(value++, 2, 3)) }, // calcs square
				t2 { power(Triple(value++, 4, 5), exp) }, //
				t3 { power(TripleInt(value++, 2, 3), exp) } //
{
}

La aplicación mínima para Arduino tiene solo un espacio en blanco configurar y círculo () funciones que ocupan 450 bytes en Uno. La círculo () utilizado para esta prueba es un poco más que mínimo, pero solo crea un ejemplo de una aplicación y lo llama círculo () función miembro:

void loop() {
	rm::Application app(2, 3);
	rm::Application::loop();	// does nothing
}

Resultados del tamaño del código

Estos son los resultados para varias combinaciones de instanciación implícita y explícita con diferentes números de variables de miembro de clase:

Las primeras columnas especifican cuántas variables se incluyeron en el Solicitud clase. Las columnas debajo de Uno y Due son el tamaño del código para estos procesadores. Muestran el tamaño de la instanciación implícita, la instanciación explícita de energía () por solo el Triple clase, y instanciación explícita para int y Triple tipos de datos.

Los tamaños de los códigos dependen de algunos factores, por lo que solo pueden dar una idea general de los cambios cuando se cambia de implícita a la creación de plantilla explícita. Los resultados reales dependen del compilador y las herramientas de enlace. Algo de esto sucede aquí con la cadena de herramientas Arduino GCC.

En todos los casos con Uno, donde se usa una variable, el tamaño del código aumenta con la instantaneidad explícita. En este caso, el código de la función más el código para nombrar la función es, como se esperaba, más grande que el código de la función en línea.

Ahora mire el lado Uno de la matriz, donde hay 2 enteros y 2 Tríos, es decir, la cuarta línea. Los dos primeros tamaños de código siguen siendo los mismos en 928 bytes. El compilador optimizó el código para ambos Trillizos () por creación energía () fuera de línea sin que se le indique explícitamente que lo haga. En la tercera columna hay una disminución en el tamaño del código cuando la versión entera de energía () se crea una instancia explícita. Hizo el mismo par de líneas a continuación que cuando solo tiene 2 Tríos. Estos fueron verificados examinando el código ensamblador. objdump en el archivo ELF.

En general, el tamaño del código de Due no mejoró con instantaneidad explícita. El tamaño de palabra más grande del Due requiere menos código para nombrar una función. Necesitaría una función mayor que energía () para hacer explícito instantáneo efectivo en este escenario.

Como mencioné, no saque demasiadas conclusiones para estos tamaños de código. Repetidamente necesité verificar el contenido del archivo ELF a través de objdump para verificar mis conclusiones. Como ejemplo, mire el lado debido, con 2 enteros y un Triple, con los dos tamaños de código de 10092. Son solo una coincidencia. En una, la versión entera de energía () está en línea y en el otro, explícitamente fuera de línea. Lo mismo sucede en la primera línea debajo de Uno, donde solo hay dos enteros y no Tríos.

Puede encontrar otros factores que influyen en el tamaño del código. Cuando tres Tríos están involucrados el compilador genera el código de multiplicación de energía (), pero no toda la función. Esto no es porque energía () es una función de plantilla pero solo una optimización general, es decir, extraer código desde dentro de un bucle.

Envoltura

Las plantillas son una parte fascinante de C ++ con capacidades extremadamente poderosas. Como se mencionó anteriormente, puede escribir un programa completo en plantillas, de modo que el compilador realmente haga el cálculo. La realidad es que probablemente no creará plantillas en la programación diaria. Son más adecuados para desarrollar bibliotecas y servidores en general. Ambas cosas energía () y Triple entra en esa categoría o está cerca de ella. Es por eso que las bibliotecas de C ++ constan de tantas clases de plantillas. La creación de una biblioteca requiere atención a los detalles más allá de la codificación normal.

Es importante comprender los patrones incluso si no los está escribiendo. Discutimos algunas de las implicaciones del uso y las técnicas para optimizar el uso de plantillas porque son una parte inherente del lenguaje. Con tanto de C ++, nos pondremos en contacto con ellos para tratar los lugares donde se pueden usar.

El proyecto Input C ++

En La-Tecnologia.io, creé Entrada C ++proyecto. El proyecto mantendrá una lista de estos artículos en la descripción del proyecto como una forma de tabla de contenido. Cada artículo tendrá una entrada de proyecto para discusión adicional. Las partes interesadas pueden profundizar en los temas, plantear preguntas y compartir resultados adicionales.

El proyecto también servirá como lugar para material adicional mío o de colaboradores. Por ejemplo, alguien podría querer tomar el código e informar los resultados a otras placas Arduino o incluso a otros sistemas integrados. Ven y mira que pasa.

  • Gravis dice:

    solo una nota, pero para reducir el tamaño de sus binarios, puede eliminar algunas funciones que quizás no esté usando.
    deshabilitar intentar / capturar: -sin excepciones

    deshabilitar la transmisión de tipo dinámico: -fno-rtti

    • Gravis dice:

      para ahorrar más espacio e información: http://forums.parallella.org/viewtopic.php?f=49&t=2327

  • el gancho dice:

    https://en.wikipedia.org/wiki/Metaprogramming

    No estoy seguro si las plantillas son para bibliotecas ... Todo el meta-problema está dirigido a la evocación.

    • PWalsh dice:

      Las bibliotecas de aceleradores realizan algunas funciones realmente interesantes. Cosas que no son inmediatamente obvias o triviales, como encontrar el elemento más lejano (la mayoría de los saltos) de un elemento seleccionado en una red de elementos vinculados.

      Gran parte de su magia se hace con plantillas, y hacen todo lo posible para que su código compile cualquier implementación compatible con C ++. Si su compilador se ajusta a C ++, las bibliotecas del acelerador funcionarán.

      Haga un solo error tipográfico en la definición y obtendrá 1500 mensajes de error de sintaxis que dicen lo mismo y se refieren a ese número de línea en algún lugar de su sistema. Es casi imposible a) depurar el problema yb) averiguar qué hizo mal.

      Por mucho que me guste la función Boost, la depuración de errores de plantilla presenta un ritmo muy alto para el codificador experto y un problema insuperable para el novato.

      Las plantillas son una buena idea, pero no es algo que yo usaría en una biblioteca para que lo usen otros.

      • Somun dice:

        Con suerte, las cosas mejorarán mucho cuando los conceptos pasen a formar parte del estándar C ++.

  • salmón dice:

    "La realidad es que probablemente no creará plantillas en la programación diaria"
    La realidad es que hago esto todos los días (este es un código de producto, no una biblioteca). Las plantillas brindan una flexibilidad sin precedentes.

    ¿También puede explicar "Las plantillas son básicamente código en línea"? Si tiene una función de plantilla y llama a esa función en otra, ¿está diciendo que la llamada a la función se reemplazará con el contenido de la función de plantilla? Eso no suena bien. Depende del compilador decidir si realmente insertará una función incluso si se especifica explícitamente una en línea en el código.

    • Rud Merriam dice:

      ¿Puedes profundizar en cómo usas las plantillas?

      Una plantilla es básicamente en línea, porque si tiene la definición de una función miembro fuera de la clase en un archivo de encabezado, debe marcarla como "en línea" o obtendrá errores de enlace en muchas definiciones. No es necesario marcar las funciones de los miembros de la plantilla.

      class Triple {
      public:
      Triple(const int x, const int y, const int z);
      //...
      };
      Triple::Triple(const int x, const int y, const int z) ...

      generará múltiples definiciones del constructor. Pero ...

      template
      class Triple {
      public:
      Triple(const T& x, const T& y, const T& z);
      //...
      };

      patrón
      Triple :: Triple (const T & x, const T & y, const T & z) ...

      no.

      • Somun dice:

        Si tiene una definición de método dentro de un encabezado e incluye ese encabezado de varios archivos, por supuesto obtendrá un error de definición múltiple. Para las clases de plantilla, la definición no está completa hasta que se crea. Pero esto es diferente de "en línea".

        Si una función está en línea, significa que la llamada a esa función será reemplazada por la implementación de esa función. En su código de muestra, el compilador puede haber elegido con gusto insertar esas funciones de plantilla power () pero dígame si tengo una función de plantilla muy grande a la que llamo desde muchos lugares del código. ¿Estaría en línea? Lo necesitamos. En este sentido, sería incorrecto decir que los patrones están esencialmente en línea. Espero que tenga sentido.
        —————————————–
        Siempre uso plantillas porque me permiten escribir código más flexible según los tipos. Por ejemplo, necesito una función para encontrar un elemento que satisfaga ciertas condiciones. Digamos que este elemento está dentro de un vector. Implemento la función para tomar un argumento de plantilla de un contenedor, en lugar de especificar explícitamente un vector como tipo. Esto me permite cambiar el tipo de contenedor sin cambiar la función. Por supuesto, primero trataría de ver si la biblioteca estándar primero tiene un algoritmo aplicable. La mayoría de las veces existe (por ejemplo, std :: find_if) y lo usaría en su lugar 🙂 Pero entiendes la idea.

        • Rud Merriam dice:

          Bueno, veo adónde va la confusión. La palabra clave "en línea" tiene dos implicaciones. Uno es una "pista" de dónde crear el código. La otra es cómo gestionar una resolución de nombres. Con funciones regulares, "en línea" le dice a la cadena de herramientas (compilador y enlace) que no se queje de los homónimos del mismo nombre. Las plantillas básicamente tienen esta característica. Mi artículo se centró mucho en los detalles de la generación de código, pero no discutió explícitamente los aspectos del conflicto de nombres. En parte, eso se debe a que no estoy tratando de hacer una presentación de abogado de idiomas sobre C ++, sino una explicación pragmática.
          —-
          Estoy de acuerdo en que no está creando bibliotecas, pero está creando una función útil que es una buena idea. Incluso para funciones simples, la creación de una plantilla podría ser útil, especialmente si la función es aplicable a todos los tipos de números.

          Paco ...

          • Somun dice:

            Rud, gracias por el intento explicativo, pero creo que podría estar confundido acerca de la intención de inline. Solo hay una implicación de inline y es sugerir al compilador sobre su preferencia para reemplazar el contenido de la función en lugar de llamar. Consulte http://en.cppreference.com/w/cpp/language/inline

          • Rud Merriam dice:

            La primera entrada debajo de una descripción dice:
            1) Puede haber más de una definición de una función en línea en el programa siempre que cada definición aparezca en una unidad de traducción diferente. Por ejemplo, una función en línea se puede definir en un archivo de encabezado # incluido en varios archivos de origen.

            Eso es lo que estaba buscando. Una función no lineal no puede tener definiciones en varias unidades de traducción. Esto conduce a errores de enlace en la duplicación.

    • kratz dice:

      Como usuario de .Net, estoy de acuerdo con la parte diaria. Cuando se compara el código .NET 1.1 con las clases generales de tipo duro apropiadas, se ve como de día y de noche.

  • Adrian dice:

    Es posible que ya tenga esto alineado para su próximo artículo: Las plantillas son excelentes para encapsular dispositivos laterales personalizables. Le permiten reemplazar la lógica #define compleja y / o el espacio de código desperdiciado debido a muchas funciones if / then en generalizadas (como las bibliotecas centrales de Arduino), al tiempo que combinan el rendimiento del primero con la conveniencia del segundo.

    Consulte este artículo (algo desactualizado) para ver un ejemplo de encapsulación de STM32 GPIO con plantillas:
    http://www.webalice.it/fede.tft/stm32/stm32_gpio_and_template_metaprogramming.html

    Aquí hay una biblioteca de plantillas para MSP430 basada en este concepto:
    https://github.com/RickKimball/msp430_code

    • Rud Merriam dice:

      Gracias por los enlaces, los revisaré. Los comentarios sobre uno de los artículos anteriores también sugirieron el uso de plantillas para encapsular GPIO. Los miraré a todos para ver qué se me ocurre.

Manuel Gómez
Manuel Gómez

Deja una respuesta

Tu dirección de correo electrónico no será publicada.