Introducción a la programación genérica (III)

Artículos de la serie:

Plantillas de clase


Como aspecto esencial de nuestro diseño, conviene valorar más detenidamente el tipo elemental a utilizar para almacenar en memoria las partes real e imaginaria de un número complejo. Ciertamente, debe utilizarse una representación de coma flotante como en el código expuesto en el segundo post. Tengamos en cuenta, sin embargo, que el usuario de la biblioteca puede desear escoger entre los distintos tipos disponibles (float, double, long double) en función de la precisión requerida por sus cálculos y la plataforma en que esté trabajando. Por ello, definiremos una clase genérica (plantilla de clase, class template en Inglés [1]) como modo de proporcionar, no una única clase Complex en la que el tipo elemental de representación subyacente (hasta ahora, double) quede establecido de antemano por el diseñador de la clase, sino toda una familia uniparamétrica de clases en la que el modo de representación pueda ser escogido por el usuario de entre los tres tipos mencionados:

   template<typename T> // el parámetro T de la plantilla es un tipo...       requires std::floating_point<T> // ...sometido a una ligadura (C++20)    class Complex {       T re_,         im_;    public:       constexpr Complex(T re = T{}, T im = T{}) noexcept          : re_{re}, im_{im} { }       // acceso:       constexpr auto real() const noexcept -> T { return re_; }       constexpr auto imag() const noexcept -> T { return im_; }       constexpr void real(T re) noexcept { re_ = re; }       constexpr void imag(T im) noexcept { im_ = im; }       // norma al cuadrado, complejo conjugado y argumento:       constexpr auto norm() const noexcept -> T       { return re_*re_ + im_*im_; }       constexpr auto conj() const noexcept -> Complex { return {re_, -im_}; }                 auto arg()  const noexcept -> T       { return std::atan2(im_, re_); }    };

La sintaxis empleada es análoga a la utilizada para clases ordinarias, salvo por el hecho de que el tipo de dato subyacente ha recibido ahora el nombre genérico T y la declaración de la clase viene precedida por la cabecera

   template<typename T>       requires std::floating_point<T>    class Complex {       // ...    };

La lista de parámetros de la plantilla (sólo se requiere uno en nuestro caso) se encierra entre corchetes angulares, siendo precedida por la palabra clave template. La palabra clave typename indica que T es un tipo arbitrario, mientras que la cláusula requires impone restricciones sobre éste. En nuestro caso, el concepto std::floating_point disponible en la cabecera estándar <concepts> (C++20) comprueba que el parámetro de la plantilla T sea de tipo floatdouble o long double [2]. De no ser éste el caso, se emitirá un mensaje de error en tiempo de compilación. Es posible simplificar la declaración anterior y escribir:

   template<std::floating_point T>    class Complex {       // ...    };

Llegados a este punto, un usuario puede probar a generar objetos como los siguientes:

   auto a_1 = Complex<float>{3.0f4.0f};  // a_1 = (3,4) de tipo Complex<float>    auto a_2 = Complex{3.0f4.0f};        // ídem    auto n_a = a_1.norm();          // n_a = 25.0f es de tipo float    auto b_1 = Complex<double>{8.0, -3.0};  // b_1 = (8,-3) de tipo Complex<double>    auto b_2 = Complex{8.0, -3.0};          // ídem    auto n_b = b_1.norm();          // n_b = 73.0 es de tipo double    auto c_1 = Complex<long double>{-6.0L}; // c_1 = (-6,0) de tipo Complex<long double>    auto c_2 = Complex{-6.0L};              // ídem    auto n_c = c_1.norm();          // n_c = 36.0L es de tipo long double

En el ejemplo anterior, los objetos a_1, b_1 y c_1 son de tipos distintos Complex<float>, Complex<double> y Complex<long double>, respectivamente. A grandes rasgos, y tomando el caso de a_1 como ejemplo, el compilador generará la instancia específica Complex<float> de la plantilla Complex reemplazando cada aparición del parámetro T con el tipo float seleccionado por el usuario [3].

El tamaño en memoria de tales objetos dependerá de la plataforma y compilador utilizados. Típicamente, el tamaño de una variable float será de 4 bytes y el de una variable double de 8 bytes, por lo que un objeto de tipo Complex<float> ocupará 8 bytes y uno de tipo Complex<double> 16 bytes. Por su parte, una variable long double podrá ocupar entre ocho y dieciséis bytes.

A partir de C++17, la técnica CTAD [4] permite deducir el parámetro de la plantilla T a partir de los tipos de los argumentos pasados al constructor de Complex. Así sucede en la inicialización de los objetos a_2b_2 y c_2, de tipo Complex<float>Complex<double> y Complex<long double>, respectivamente.

Para finalizar, observemos que la inicialización de objetos como los siguientes produciría errores de compilación, pues int y std::string no son tipos que cumplan el concepto std::floating_point y, por tanto, no son permitidos como parámetros de la plantilla Complex:

   auto d = Complex<int>{4, 3}; // error de compilación: incumplimiento de ligadura    auto e = Complex<std::string>{"hello", "world"}; // nuevo error de compilación



Referencias bibliográficas:
  1. Class template - https://en.cppreference.com/w/cpp/language/class_template
  2. std::floating_point - https://en.cppreference.com/w/cpp/concepts
  3. Microsoft - Plantillas en C++ - https://docs.microsoft.com/es-es/cpp/cpp/templates-cpp?view=vs-2019
  4. CTAD - https://en.cppreference.com/w/cpp/language/class_template_argument_deduction

Puedes acceder al siguiente artículo de la serie a través de este enlace.

2 comentarios: