El propósito de las templates:
Como vimos en en el anterior capítulo, las templates son la herramienta que provee C++ para implementar el paradigma de la programación genérica. Su propósito original era proveer de estructuras de datos verdaderamente genéricas, sin tener que lidiar con los engorros de void*
heredados de C.
Además de parametrizar tipos, las templates de C++ permiten parametrizar valores evaluables en tiempo de compilación, como números enteros por ejemplo. Esto puede resultar muy útil en ciertos casos, el wrapper de array que vimos en el anterior capítulo es uno de ellos:
template<typename T>
class array_wrapper
{
private:
T _array[10];
public:
T& operator[](size_t index)
{
if(index >= 10) throw "Index out of bounds";
return _array[index];
}
const T& operator[](size_t index) const
{
if(index >= 10) throw "Index out of bounds";
return _array[index];
}
};
Como ya hice notar al presentar esta class template, el principal problema que tiene es que solo permite simular un array de diez elementos. Lo ideal sería que pudiéramos elegir el tamaño del array. Este problema es fácilmente solucionado añadiendo un parámetro de valor a la plantilla:
template<typename T , size_t LENGTH>
class array_wrapper
{
private:
T _array[LENGTH];
public:
T& operator[](size_t index)
{
if(index >= LENGTH) throw "Index out of bounds";
return _array[index];
}
const T& operator[](size_t index) const
{
if(index >= LENGTH) throw "Index out of bounds";
return _array[index];
}
};
Así el usuario de esta template class puede especificar el tipo y el tamaño del array que más le convenga. Por ejemplo:
int main()
{
array_wrapper<int,5> my_array;
my_array[0] = 2;
std::cout << my_array[0] << std::endl;
}
Al ejecutarse la salida del programa sería la siguiente:
2
Este ejemplo de wrapper de array no es únicamente anecdótico, es un contenedor muy utilizado que provee la librería estándar: std::array
. Hoy en día se considera muy buena práctica utilizar std::array
en lugar de los arrays de C, ya que std::array
no es implícitamente convertible a un puntero, lo que elimina ciertos problemas de seguridad que dicha conversión provoca. Además std::array
provee la misma interfaz que el resto de contenedores de la librería estándar.
A lo largo de los años fue haciéndose evidente que las capacidades de las templates iban mucho más allá, hasta tal punto que se ha demostrado en varias ocasiones que son Turing Completo. Este hecho es la base para una de las prácticas más potentes que C++ provee, template-metaprogramming, que examinaremos en profundidad en capítulos posteriores.
Sin llegar a los extremos de crear verdaderos programas ejecutables en tiempo de compilación a base de template-metaprogramming, si es cierto que las templates son un gran mecanismo para parametrizar y automatizar la generación de código, rasgo que le da su verdadera importancia en aplicaciones reales.
Las templates de C++ permiten regenerar la implementación completa de nuestras clases y funciones, adaptándolas a nuestras necesidades, con solo cambiar un par de parámetros. Los contenedores de la librería estándar son un buen ejemplo de ello: El allocator que usa el contenedor está parametrizado, así que puedes proveer el que quieras, o implementar uno propio acorde a lo que necesites.
La sintaxis de las templates en profundidad
La sintaxis de una template, tanto para clases como funciones, es la siguiente:
template<lista de parámetros> declaración de clase/función
Donde los parámetros, separados por comas, pueden ser de dos tipos:
Type parameters
El parámetro representa un tipo. Se especifican con la sintaxis ” typename/class nombre_del_parámetro “.
C++ acepta tanto la palabra clave “typename” como la palabra clave “class” para indicar pàrámetros de tipo. Es decir, es lo mismo escribir la template así:
template<typename T>
que así:
template<class T>
Ambas son plantillas con un parámetro de tipo.
La diferencia es púramente histórica: En la primera especificación de templates, Stroupstrup recicló la palabra clave class para utilizarla también como especificador de tipo en las plantillas. Así no tenía que añadir una palabra clave nueva al lenguaje y hacer incompatible el código antiguo. Lo malo es que dicha palabra puede conducir a errores, ya que al leer class cualquier persona inexperta podría pensar que dicho parámetro solo permite clases, y no tipos de datos básicos.
Al comenzar la estandarización de C++ a mediados de los noventa, se encontraron ciertos contextos en los cuales podían generarse ambiguedades al usar parámetros de tipo. Por ejemplo:
template<typename T>
class Foo
{
T::a *pointer_of_type_a;
};
Como es lógico, la intención del programador es declarar un puntero cuyo tipo es una clase interna a la clase pasada como parámetro. Como cuando necesitamos iterar sobre un vector: std::vector<T>::iterator it = v.begin();
Nosotros sabemos que iterator
es un tipo, un tipo definido dentro de la clase vector
, pero el compilador no puede estar seguro. También puede ser una variable estática. La expresión es ambigua sintácticamente. Nótese que en el caso de que se esté usando un tipo concreto, std::vector<int>
por ejemplo, dicha ambigüedad ya no se produce.
En efecto en el ejemplo que estabamos viendo el compilador interpreta la expresión como una operación aritmética entre un atributo estático de T y una variable de nombre “pointer_of_type_a”.
Para resolver este tipo de ambigüedades, el comité de estandarización decidió añadir una nueva palabra clave al lenguaje, typename, con el propósito de indicar cuando cierta expresión hace referencia a un tipo:
template<typename T>
class Foo
{
typename T::a *pointer_of_type_a; //everything ok. T::a is a type. Now compiler understands.
};
Aprovechando la nueva palabra clave, arreglaron el desaguisado que había supuesto el uso de class
como especificador de parámetros de tipo. typename
es mucho más legible, y no da lugar a confusiones. Personalmente, siempre uso typename
.
Non-type parameters
El parámetro representa un valor evaluable en tiempo de compilación. Dicho valor puede ser de los siguientes tipos:
-
Un valor entero: Cualquier valor entero que sea evaluable en tiempo de compilación, esto es, resultado de una operación evaluada en tiempo de compilación o un valor entero propiamente dicho.
Ejemplos:template<usnigned int n> struct Foo {}; struct Bar { static const unsigned int a = 10; const unsigned int b; Bar(unsigned int _b) : b( _b ) {} }; enum ENUM {ENUM_1 , ENUM_2}; int main() { unsigned int i = 12; Bar bar( i ); using foo_instance_1 = Foo<0>; //OK using foo_instance_2 = Foo<2+2>; //OK using foo_instance_3 = Foo<Bar::a>; //OK using foo_instance_4 = Foo<(Bar::a > 2) ? 1 : 0>; //OK using foo_instance_5 = Foo<sizeof( bool )>; //OK using foo_instance_6 = Foo<ENUM_1>; //OK (Ver nota) using foo_instance_7 = Foo<bar.b>; //ERROR using foo_instance_8 = Foo<i>; //ERROR }
NOTA: El compilador realizará las conversiones implícitas necesarias para “encajar” el valor del parámetro. En este caso los valores de las enumeraciones son
int
, y el parámetrounsigned int
. Algunos compiladores, pero no todos, generan warnings al instanciar templates que utilizan este tipo de conversiones potencialmente peligrosas (int
aunsigned int
,long int
aint
, etc). Por supuesto las conversiones explícitas también son válidas.
Nótese que bool
también se considera entero, al igual que char
(Realmente son enteros ):
template<bool FLAG , char CHARACTER>
struct Foo{};
– Un puntero a una función con linkage: Esto descarta funciones que no sean globales ni funciones estáticas de una clase.
– Un puntero a un dato con linkage: Esto descarta cualquier dato que no sea global, y dato miembro no estático de una clase. Nótese que esto implica que no pueden utilizarse raw-strings como parámetros de una plantilla. Los arrays globales son implícitamente convertidos a puntero, y por tanto si pueden ser utilizados, pero no un puntero a un elemento del array, ya que este no tiene linkage.
– Referencia a lvalue con linkage: Al igual que en el caso de los punteros, descarta cualquier variable no global o miembro estático, además de no ser válidas referencias a objetos temporales (rvalues).
– Puntero a miembro de clase: Dicho puntero debe ser de la forma &Clase::miembro
, es decir, solo son válidos punteros a funciones miembro y a atributos estáticos.
Como puede verse, exceptuando los tipos enteros, la validez de los argumentos depende directamente de si dicho argumento posee linkage o no. El linkage indica como los nombres pueden hacer referencia o no a un mismo elemento a lo largo de un módulo o el programa completo. Es lógico que el linkage de este tipo de elementos sea necesario en los parámetros de las templates, ya que dichos elementos deben ser conocidos y accesibles en tiempo de compilación.
Template template parameters
Son un tipo especial de type-parameters los cuales especifican que el tipo es una class template. Su sintaxis es template<typename> class nombre_del_parámetro
. Por ejemplo:
template<template<typename> class T>
struct Foo;
template<typename T>
struct Bar;
using foo_alias = Foo<Bar>;
Por supuesto dicha class template puede tener los parámetros que necesitemos. Es decir, se aplican las mismas reglas que una template (Y así recursivamente). Veamoslo con un ejemplo algo enrevesado:
template<template<template<typename,unsigned int> class> class T>
struct Foo {};
template<template<typename,unsigned int> class T>
struct Bar {};
using foo = Foo<Bar>;
Foo
es una class template cuyo parámetro debe ser una class template con un type-parameter como primer parámetro y un entero sin signo como segundo parámetro.
¿Para que son útiles los template template parameters? Principalmente para especialización de templates un concepto que trataremos en capítulos posteriores.
Pero también existen otros casos en los que pueden resultar útiles. Por ejemplo, una template que nos devuelve una instancia concreta de un contenedor que se le pasa como parámetro:
template<template<typename> class CONTAINER_TYPE>
struct bool_container
{
using type = CONTAINER_TYPE<bool>;
};
using my_bool_vector = typename bool_container<std::vector>::type; //my_bool_vector es std::vector<bool>;
Este ejemplo es púramente ilustrativo, en realidad no funcionaría: std::vector
tiene dos parámetros, el tipo y el allocator, y bool_container
espera una plantilla de un solo argumento. De echo, los contenedores de la librería estándar tienen un número de argumentos muy variados, así que hacer esto de verdad no sería tan sencillo. La solución sería hacer que CONTAINER_TYPE
fuera una variadic-template (Ver apartado siguiente).
Extras: Argmentos opcionales y variadic templates
Además de especificar una lista de parámetros de plantilla, C++ permite especificar parámetros opcionales y conjuntos de un número indeterminado de parámetros.
Parámetros opcionales
Al igual que con los parámetros de las funciones, podemos especificar parámetros de plantilla opcionales, con valores predeterminados:
template<typename T = int , typename U = bool>
struct Foo;
int main()
{
Foo<> foo; //Ojo! <> es necesario
}
Por supuesto, al igual que en las declaraciones de funciones, los parámetros opcionales deben aparecer al final.
Esta característica es altamente explotada por los contenedores de la librería estandar: El ejemplo que veíamos antes del allocator. Dicho allocator está parametrizado, si, pero tiene como valor predeterminado el allocator predefinido por la librería estandar, para que no tengamos que especificarlo manualmente.
Los parámetros opcionales únicamente estaban permitidos para class templates hasta que C++11 añadió esta característica para funciones también. Según palabras del propio Stroupstrup:
The prohibition of default template arguments for function templates is a misbegotten remnant of the time where freestanding functions were treated as second class citizens and required all template arguments to be deduced from the function arguments rather than specified.
The restriction seriously cramps programming style by unnecessarily making freestanding functions different from member functions, thus making it harder to write STL-style code.
Variadic templates
Las variadic templates son una nueva característica de C++11 que permite utilizar tempates con un número indeterminado de parámetros (Ninguno inclusive). La sintaxis es la siguiente:
template<typename... Ts>
struct Foo{};
O con non-type params:
template<unsigned int... Ns>
struct Foo{};
Los puntos suspensivos, ...
, indican que el parámetro en cuestión no es un parámetro de pantilla, sino lo que se denomina “variadic pack”. Esto es muy importante: Un variadic pack no es un parámetro de plantilla, y no puede ser tratado como tal. No es más que una representación abstracta de un conjunto no determinado de parámetros.
Dentro de una class template podemos definir alias de tipos utilizando como base los parámetros de tipo de la plantilla, como hacen los contenedores de la librería estandar por ejemplo:
template<typename KEY , typename VALUE>
class unordered_map_traits
{
using key_type = KEY;
using value_type = VALUE;
using pair_type = std::pair<KEY,VALUE>;
};
Esto no se puede hacer con un variadic pack, ya que como hemos dicho no es un tipo:
template<typename... Ts>
struct Foo
{
using types = Ts; //ERROR
};
Este problema puede solucionarse guardando el variadic pack en una typelist, una construcción que estudiaremos más adelante.
Lo que si podemos hacer es averiguar cuantos parámetros forman el paquete: Para ello C++ provee el operador sizeof...
:
template<unsigned int... Ns>
struct Foo
{
static const size_t number_of_tparams = sizeof...Ns;
};
Así podemos saber cuántos parámetros se han pasado al instanciar la plantilla.
Como ya he dicho, los variadic-packs no son más que una representación abstracta de un conjunto de parámetros. Así pues, podemos pasar dicho conjunto de perámetros a otro lado. Para ello, necesitamos desempaquetar el conjunto, y hacer que el compilador lo trate como un conjunto de parámetros per se. Veamos un ejemplo:
template<typename... Ts>
struct Foo;
template<typename... Ts>
using foo_alias = Foo<Ts...>;
Como podemos ver, los puntos suspensivos ...
se utilizan para desenpaquetar el conjunto de parámetros y pasárselos a otra plantilla. Esto mismo es válido para function templates con variadic templates:
template<typename... Ts>
void f(const Ts& args...)
{
return f(args...); //Recursividad infinita
}
En primer lugar deinimos los argumentos de la función, cuyo tipo es el conjunto de tipos representados por el variadic pack (Pasados como referencia constante). Expandimos para que dicha definición se aplique a todos los tipos del paquete.
Al igual que un variadic-pack no es un tipo, sino una representación abstracta de un conjunto de argumentos de plantilla, args
no es un argumento de una función es una representación abstracta de un conjunto de argumentos de función. Por tanto para poder usarse dicho conjunto de argumentos, el paquete tiene que ser expandido, como efectivamente hace la sentencia return f(args...);
.
Es muy importante entender que esos packs (Tanto los de argumento de template como los de función) no son más que representaciones abstractas de conjuntos de parámetros, y como tal tienen la misma categoría que un conjunto de parámetros que podrías haber escrito tu a mano. Veamos paso a paso como el compilador instancia una variadic template:
Tenemos la siguiente plantilla de clase:
template<typename... Ts>
struct Foo{};
int main()
{
Foo<int,char,double> my_foo_variable;
}
Esto es equivalente a que hubieramos escrito la plantilla Foo
con tres parámetros de tipo. El mecanismo de instanciación es exactamente el mismo que hemos visto hasta ahora:
template<typename T , typename U , typename W>
struct Foo{};
int main()
{
Foo<int,char,double> my_foo_variable;
}
Se traduce a:
struct __Foo_int_char_double {};
int main()
{
__Foo_int_char_double my_foo_variable;
}
La única diferencia es que la variadic template no nos exige introducir exactamente tres tipos, sino que nos permite introducir el número de tipos que queramos. Es decir, la template es aún más genérica.
Como he dicho antes, los variadic-packs tienen la misma categoría que un conjunto de parámetros que hubiéramos escrito a mano. Porque realmente es lo que son, un conjunto abstracto de esos parámetros. Esto quiere decir que podemos combinar variadic-packs con listas de parámetros escritos a mano sin ningún problema. Por ejemplo:
template<typename... Ts>
struct Foo{};
template<typename... Ts>
using my_Foo_alias_1 = Foo<int,Ts...>;
template<typename... Ts>
using my_Foo_alias_1 = Foo<Ts...,char,bool>;
template<typename... Ts>
using my_Foo_alias_1 = Foo<Ts...,char,Ts...>; //Le pasamos dos veces, por qué no?
int main()
{
my_Foo_alias_1<char,char,char> a; //a es de tipo Foo<int,char,char,char>
my_Foo_alias_1<> b; //b es de tipo Foo<int>
my_Foo_alias_2<int,int> c; //c es de tipo Foo<int,int,char,bool>
my_Foo_alias_3<bool,int,float> d; //d es de tipo Foo<bool,int,float,char,bool,int,float>;
}
Por supuesto podemos escribir templates que utilicen a su vez variadic packs y parámetros normales. Por ejemplo:
template<typename T , typename U , unsigned int... Ns>
struct Foo {};
using foo_1 = Foo<int,bool,1,2,3>;
using foo_2 = Foo<int,char>;
La única limitación es que solo puede haber un variadic pack, y debe ser el último parámetro de la template. Esta limitación no existe con function templates, ya que los tipos son la mayoría de las veces deducidos en base a los argumentos de la función. Ya hablaremos de esto en profundidad más adelante.
Conclusión:
Aunque todavía quedan algunos aspectos conceptuales por explicar, en este capítulo hemos aprendido para que se utilizan las template, y hemos visto su sintaxis en profundidad.
El objetivo del capítulo era conocer el contexto en el que se usan las templates, como fin de los contenidos introductorios, y conocer con exactitud la sintaxis de las templates. Hemos visto que las templates permiten utilizar diferentes tipos de parámetros, cada uno con su uso y limitaciones. Ahora que ya conocemos esto podemos empezar a tratar esos importantes conceptos de su uso y funcionamiento más allá de la mera sintaxis.
Apéndices:
Apéndice A: ¿Cuando debo utilizar la palabra clave typename
?
Como vimos en el apartado type-parameters, la palabra clave typename
se utiliza para resolver ciertas ambigüedades sintácticas inherentes al uso de templates.
El uso de dicha palabra clave, esto es, el reconocimiento por parte del programador de si cierta expresión es ambigua o no, es uno de los conceptos más importantes (Y peor explicados en general) de las templates de C++.
¿Por qué y en que casos hay ambigüedad?
El problema es la gramática del lenguaje. C++ es conocido por ser el lenguaje de programación con la gramática más compleja, y esto deriva directamente en que los parsers de C++ son muy complejos de implementar, y la compilación no es precisamente sencilla (Tarda muuuucho tiempo). Dicha complejidad está relacionada principalmente con las templates, y con el echo que ya mencionamos de que este mecanismo es Turing Completo.
En teoría los lenguajes de programación utilizan gramáticas de tipo dos, es decir, gramáticas libres de contexto ; pero en el caso de C++ eso no es del todo cierto.
Por ejemplo: Es muy facil diseñar un programa en C++ cuya validez sintáctica depende directamente de si cierto número es primo o no:
template<bool flag , unsigned int N>
struct value_holder;
template<unsigned int N>
struct value_holder<true,N>
{
static const int value = N;
};
template<unsigned int N>
struct value_holder<false,N>
{
using value = unsigned int;
};
template<unsigned int N>
struct is_prime
{
static const bool result = /* La implementación la dejaremos para
otro día, demasiada template-metaprogramming
para ahora. Pero se puede hacer, y es
sencillo */
};
template<unsigned int N>
struct Foo : public value_holder<is_prime<N>::value,N> {};
int main()
{
unsigned int i = Foo<7>::value; //Esto compila
unsigned int j = Foo<4>::value; //Esto no compila
}
Este ejemplo demuestra que:
– El mecanismo de instanciación de las templates es Turing Completo (Algo que ya sabíamos).
– La gramática de C++ no puede ser estrictamente de tipo dos, ya que este tipo de condición (Validez sintáctica dependiente de primalidad) no puede ser expresada con este tipo de gramáticas.
En efecto existen casos en los que la validez sintáctica de una sentencia depende directamente del contexto de esta, y en muchos de ellos dicho contexto no puede ser determinado por el compilador directamente, con lo que se genera la ambigüedad.
Hay casos en los cuales el compilador utiliza lo que se denomina most vexing parse, una situación en la cual la ambigüedad es eliminada en base a una regla del estándar que produce resultados inesperados para un programador novel. La regla es la siguiente:
Cualquier situación en la cual una expresión puede ser evaluada tanto como una declaración, como alguna otra cosa; la expresión deberá ser evaluada como una declaración.
Esto produce que la siguiente expresión:
struct Foo {};
struct Bar
{
Bar(const Foo&); //Bar ctor
};
int main()
{
Bar bar(Foo()); //Inicialización llamando al constructor de Foo.
}
sea evaluada como la declaración de una función llamada bar
que devuelve Bar
por valorque tiene como parámetro (Parámetro sin nombre) una función sin parámetros que devuelve Foo
por valor. Este en problema en concreto se soluciona muy fácilmente en C++11 utilizando uniform initializers:
int main()
{
Bar bar{Foo()}; //Inicialización llamando al constructor de Foo. OK (no most vexing parse)
Bar bar{Foo{}}; //Mejor (Coherencia sintáctica/estilo)
}
Una situación similar ocurría al utilizar templates: Debido a como el parser tokeniza el código fuente, el compilador entendía >>
como operator>>
no como el cierre de dos templates.
Pero también hay muchos casos en los que el compilador no es capaz por si solo de resolver la ambigüedad. Uno de los más importantes es el uso de nombres dependientes de templates
Qualified-ids, non-qualified-ids y nested-name-specifiers
Un qualified-id es un identificador que contiene algún tipo de marca o señal que indica en que lugar está declarado, es decir, a que ámbito pertenece. Un nested-name-specifier es precisamente la parte de un qualified-id que indica el ámbito de este. Veámos un par de ejemplos:
namespace foo
{
struct Bar
{
void quux();
}
}
– foo
es un unqualified-id
– ::foo
es un qualified-id sin nested-name-specifier (::
sin nested-name-specifier denota el ámbito actual).
– foo::Bar
es un qualified-id, y foo::
es su nested-name-specifier.
– foo::Bar::quux()
es un qualified-id y tanto Bar::
como foo::
como foo::Bar::
son nested-name-specifiers.
Qualified-ids y templates: Ambigüedad y typename
Veamos el siguiente ejemplo:
struct Foo
{
using type = int;
};
template<typename T>
struct Bar
{
using type = T::type; //ERROR (type es un tipo o que?)
};
¿Que pasa al hacer referencia al qualified-id T::type
? Ambigüedad El compilador no puede saber por si mismo si type
será un tipo o cualquier otra cosa, ya que no sabe que es T
, y por tanto que será type
. Por supuesto si al instanciar la plantilla el parámetro no tiene un miembro type
, la instanciación fallará.
Para asegurar al compilador que en ese contexto type
tiene que ser un tipo, utilizamos la palabra clave typename
:
template<typename T>
struct Bar
{
using type = typename T::type; //OK: type es un tipo (Si no lo es, error)
};
Esto mismo ocurre en cualquier contexto en el cual un nested-name-specifier dependa de un parámetro de plantilla. Por ejemplo:
template<typename T>
using vector_value_type = std::vector<T>::value_type; //ERROR (Que es value_type?)
En este tipo de casos typename
es necesario porque, aunque conocemos la plantilla, no conocemos la instancia de dicha plantilla, y value_type
puede ser una cosa en una instancia, y otra en otras. Esto se debe a que la plantilla puede estar especializada, un mecanismo que estudiaremos en profundidad más adelante.
La sintaxis correcta sería:
template<typename T>
using vector_value_type = typename std::vector<T>::value_type; //OK: value_type tiene que ser un tipo. (Si no lo es, error)
En concreto, el estandar dice lo siguiente:
A name used in a template declaration or definition and that is dependent on a template-parameter is assumed not to name a type unless the applicable name lookup finds a type name or the name is qualified by the keyword typename.
Apéndice B: ¿Cuando debo utilizar la palabra clave template
?
Consideremos la siguiente expresión:
std::function<int()> f;
Obviamente es la declaración de un functor llamado f
, que representa a una función sin parámetros que devuelve int
por valor. ¿Obviamente? Para el compilador no es tan obvio. Veamos la siguiente declaración:
namespace std
{
int function = 0;
}
¿Y ahora que? Si esa es la declaración de std::function
, la expresión anterior se evalúa como una comparación entre std::function
y cero (int()
es una llamada a la inicialización de int
, que es cero), y el resultado de dicha comparación es comparado con f
.
Por supuesto en la vida real std::function
es una template, y el compilador así lo reconoce (Estándar 12.2/3):
After name lookup (3.4) finds that a name is a template-name, if this name is followed by a <, the < is always taken as the beginning of a template-argument-list and never as a name followed by the less-than operator
Pero, y si el nombre depende de una template y el compilador no puede estar seguro de que el nombre al que nos refereimos es una template? Por ejemplo:
struct Foo
{
template<bool flag>
void quux(); //quux es una function template
};
template<typename T>
struct Bar
{
T variable;
void execute()
{
variable.quux<false>(); /* ERROR: El compilador no sabe si quux es una
template function /*
}
};
using Bar_instance_using_Foo = Bar<Foo>;
Al igual que en el caso de typename
(Estamos intentando referirnos a un tipo), el problema aquí es que el nombre quux
depende de una template, y el compilador no puede saber si quux
es una function template o no (No sabe lo que es T
, y por tanto no sabe lo que es quux
). Lo primero que intenta hacer el compilador es parsear la expresión como una comparación, interpretando que quux
es un atributo.
Para indicarle que nos referimos a una template, usamos la palabra clave template
:
variable.template quux<false>(); //OK: quux es una template
Como puede verse, las razones de la ambigüedad son exactamente las mismas que en el caso de typename
: El nombre depende de una template, y por tanto el compilador no sabe lo que es ese nombre (O al menos no puede estar seguro).
Por último, existen casos en los cuales es necesario utilizar this->template
para referirse a una function template miembro de la plantilla. Estos casos ocurren cuando dicho miembro es un miembro heredado de una class template. Una vez más, si el nombre depende de una template, la palabra clave template
es necesaria para desambiguar la expresión. En un caso como este el sitio donde aplicar template
es la misma clase, es decir, this
:
template<typename T>
struct Foo
{
template<bool flag>
void quux();
};
template<typename T>
struct Bar : public Foo<T>
{
void execute()
{
quux<false>(); //ERROR: quux depende de Foo<T>, y por tanto no puedo
// saber si quux es una function template.
}
}
Solución:
this->template quux<false>(); //OK: quux es una function template.
Apéndice C: Herencia con templates y name-lookup
Cuando dentro del ámbito de una clase se hace referenia a un nombre, el compilador recorre el ámbito de dicha clase y el de sus clases bases para encontrar la definición de dicho nombre. Por ejemplo:
struct Foo
{
int x;
};
struct Bar : public Foo
{
int f()
{
return x; //Sin problema, x se encuentra en la clase base Foo
}
};
Pero existe una excepción: Las templates.
Fases de la compilación de las templates
Las templates son compiladas en dos fases:
- Antes de la sustitución de los argumentos: En esta primera fase todos aquello que no dependa de los parámetros de la plantilla es chequeado y localizado. Esto implica que los nombres dependientes de un argumento de la plantilla son evaludados en la segunda fase, cuando se realiza la instanciación y se sustituyen los parámetros por sus valores.
- Tras sustituir los parámetros por sus valores: En la segunda fase, todas aquellas construcciones y nombres que dependían de algún argumento son examinadas y chequeadas.
El problema
Modifiquemos nuestro ejemplo inicial usando templates:
template<typename T>
struct Foo
{
int x;
};
template<typename T>
struct Bar : public Foo<T>
{
int f()
{
return x; //ERROR: x no está declarado en este ámbito
}
};
De buenas a primeras no hay nada que indique que x
depende de un argumento de la plantilla. Y por tanto x
es resuelto en la primera fase. El problema está en que el ámbito de x
, Foo<T>
sì que depende de un parámetro, y por tanto el compilador no puede examinar con seguridad dicho ámbito en la primera fase.
Una solución al problema consiste en utilizar this->
para hacer que x
sea un nombre dependiente de la clase, y por tanto su análisis sea pospuesto hasta la segunda fase:
return this->x; //OK: x se convierte en dependiente, por tanto su análisis
// queda pospuesto hasta la segunda fase, y en ésta el
// compilador examina el ámbito Foo<T>, donde encuentra
// a x.
Apéndice D: ¿Por qué las templates tienen que implementarse por completo en la cabecera?
Uno de los primeros problemas que surgen con el uso de las templates es que estas no pueden escribirse siguiendo el esquema típico de declaraciones en .hpp
e implementación en .cpp
.
El problema surge por el modelo de compilación de C++ y las dos fases de la compilación de las templates.
El modelo de compilación de C++: Translation units
Una unidad de traducción es el conjunto de todo el código fuente que es incluido directa o indirectamente por un archivo de código, después de su preprocesamiento. En esencia es la unidad mínima de código que el compilador trata como un todo e intenta compilar. Es decir, por cada unidad de traducción se generará un archivo de código objeto.
Por ejemplo, al compilar el archivo main.cpp
:
#include <iostream>
int main()
{
std::cout << "hello world!" < std::endl;
}
tras preprocesar el archivo el compilador procederá a compilar todo el código que ha sido incluido en este tras el preprocesamiento, y generar con ello un archivo de código objeto. En este caso por tanto, dicha unidad de traducción incluirá el código de main.cpp
y todo el código incluido a través de <iostream>
.
Una de las razones por las cuales la compilación de código de C++ es tan lenta es porque al incluir el mismo archivo de código en varias unidades de traducción distintas, dicho código tiene que ser recompilado por completo en cada unidad de traducción, ya que su configuración y contenido puede variar dependiento de las directivas del preprocesador que incluya el archivo raiz.
Cuando se compila el típico programa de C++:
/* Foo.h */
void foo();
/* Foo.cpp */
void foo()
{
/* blah, blah, blah ... */
}
/* main.cpp */
#include "Foo.h"
int main()
{
foo();
}
g++ main.cpp Foo.cpp
El compilador comienza por compilar la unidad de traducción que incluye el punto de inicio del programa. En este caso, main.cpp
. Tras preprocesar e incluir todo el código de Foo.h
en main, se pondrá a compilar el programa.
Cuando el compilador encuentra declaraciones, y dichas declaraciones se usan (Como foo()
en este caso), lo que hace es dejar marcas en el código que usa las declaraciones, para que las vea el linker. Como diciendole “Oye en este archivo estas haciendo referencia a una cosa de la que solo tengo la declaración. Te pongo una banderita aquí por si encuentras la definición, así ya sabrás que este uso hace referencia a ese código que acabas de encontrar (La definición)”. Ese es el trabajo del linker: Unir el uso de nombres (Declaraciones) con el código de estas, para resolver adecuadamente los saltos (Llamadas) y demás.
Tras terminar de compilar la unidad de traducción de main.cpp
, el compilador continúa con la del resto de archivos a compilar, en este caso Foo.cpp
. Al compilar Foo.cpp
se encuentra con la implementación de cosas marcadas con nombres (foo()
en este caso). Compila dicha implementación y deja las marcas correspondientes en el código objeto (“Esto se llama foo()
, será la implementación de algo, digo yo”).
Al terminar de compilar el código, es el turno del linker: Este examina los distintos archivos objeto buscando correspondencias entre las marcas: “Anda! Pero si esta marca de aquí concuerda con esta banderita! O sea que esta es la implementación de foo()
y por tanto esa bandera en main.obj
a lo que hace referencia es a este cacho de código de foo.obj
. Ya se como resolver la llamada!!!”.
Si por alguna casualidad el enlazador se queda con alguna banderita sin resolver, es decir, alguna declaración en uso (Banderita) para la que no encuentra implementación, generará un error. Un error bastante común y que a la gente novel le trae verdaderos dolores de cabeza, porque los nombres ya han pasado el mangling y no saben que significa esa especie de balbuceo del compilador (Recordemos que el error se detecta en la fase de linking, es decir, trabajando con código objeto. Por eso el mangling ya está hecho, y el compilador no puede escribir el error con nombres más bonitos).
¿Y que pasa con las templates?
El probema con las templates es que, como hemos visto, se compilan en dos fases (Antes de instanciación y después de instanciación). Y como el .h está en una unidad de traducción diferente del .cpp, la unidad de traducción del .cpp no tiene ni idea de que instancia de la plantilla ha usado la otra unidad de traducción. Las unidades de traducción son completamente independientes, y lo que nosotros le estamos pidiendo es que haga la primera fase de la compilación de las templates en una, y la segunda fase la continue en la otra. Por tanto, imposible.