ApprentissageProgrammationCpp  ..
Un tour d'horizon des templates

Table des matières

Templates et fonctions

Le principe de base

Nous l'avons déjà vu dans la partie sur les fonctions, mais un petit rappel ne fait pas de mal, le principe des template en C++ est d'écrire un patron de code qui sera utilisé par le compilateur lorsqu'il en aura besoin. L'idée caché derrière les templates est de minimiser le code à écrire pour le développeur. Par exemple, si nous voulons écrire une fonction qui affiche le contenu d'un std::vector plutôt que de faire N versions de la même fonction nous pouvons utiliser les templates de la manière suivante :

template<typename T>
void printVector( const std::vector<T>& v){
for( const T& x: v){
std::cout << x << ", ";
}
std::cout << std::endl;
}

Et ensuite nous pouvons utiliser cette fonction pour n'importe quel std::vector<T>.

std::vector<int> v {1,2,3,4,5};
printVector<int>(v);

Avec du c++ moderne on peut se passer de spécifier le type de T à l'appel de la fonction. Le compilateur est malin maintenant il le déduit du contexte :

std::vector<int> v {1,2,3,4,5};

Spécialisation de template

Maintenant imaginons que nous voulions afficher en plus du contenu du std::vector<T> un message spécifiant de quel type est T. Pour cela nous allons définir une fonction myType templatée par T qui va renvoyer une chaine de caractère définissant le type de T. Il va donc nous falloir faire des versions spécifiques, une pour chaque type. C'est ce qu'on va appeler de la spécialisation de templates. Le principe est de définir la version générique par exemple ici :

template<typename T>
std::string myType(){ return ""; }

Et ensuite on spécialise la fonction template en spécifiant explicitement son comportement pour certaines valeur du paramètre template T par exemple :

template<>
std::string myType<int>(){ return "int"; }
template<>
std::string myType<double>(){ return "double"; }

Nous pouvons alors définir la fonction qui affiche le contenu d'un std::vector<T> de la manière suivante :

template<typename T>
void printVectorWithType( const std::vector<T>& v){
std::cout << "std::vector<" << myType<T>() << ">: ";
for( const T& x: v){
std::cout << x << ", ";
}
std::cout << std::endl;
}
std::vector<int> v {1,2,3,4,5};
std::vector<int>: 1, 2, 3, 4, 5
template<typename T>
std::string myType(){
if constexpr( std::is_same_v<T, int>){
return "int";
}
else if constexpr(std::is_same_v<T, double>){
return "double";
}
else{
return "";
}
}

Les templates de template

Il y a quand même une limite à la version de printVector que l'on vient de faire. Considérons par exemple le cas où l'on a un std::vector<std::vector<int>> :

std::vector<std::vector<int> > v { {1,2,3}, {4,5,6}, {7,8,9}};

Dans ce cas la fonction template que l'on a faite ne va pas bien fonctionner.... Cependant il est possible de faire une spécialisation un peu particulière :

template<typename T, template <typename, typename> class Container>
void printVector(const std::vector<Container<T, std::allocator<T>>>& v){
for( auto& x: v){
for( const T& y: x){
std::cout << y << ", ";
}
std::cout << std::endl;
}
}

Template de classes

Méthodes template

Nous venons de le voir nous pouvons templater des fonctions. Bon et bien par extension il paraît plutôt évident que l'on peut templater des méthodes de classe, car fondamentalement il s'agit de fonctions. Et bien oui je vous confirme on peut faire ça !

Par exemple :

class MyPrinter{
public:
MyPrinter(const std::string& prefix): prefix_{prefix}{
}
template<typename T>
void info(const T& data){
std::cout << this->prefix_ << data << std::endl;
}
protected:
std::string prefix_;
};
MyPrinter p("[Printer] - ");
p.info(2.34);
p.info("coucou");
[Printer] - 2.34
[Printer] - coucou
class Data{
public:
Data(const double&x, const int& n, const std::string& name): x{x}, n{n}, name{name}{}
template<typename T>
T get(){
if constexpr( std::is_same_v<T, double>){
return this->x;
}
else if constexpr( std::is_same_v<T, int> ){
return this->n;
}
else if constexpr( std::is_same_v<T, std::string>){
return this->name;
}
else{
throw std::invalid_argument("unsupported type");
}
}
protected:
double x;
int n;
std::string name;
};
Data d(1.27, 42, "toto");
std::cout << d.get<double>() << std::endl;
std::cout << d.get<int>() << std::endl;
try{
std::cout << d.get<bool>() << std::endl;
} catch( std::invalid_argument& e){
}
1.27
42

Classe template

Généralités

template<typename T>
class Number{
public:
Number(const T& x): value_{x}{}
T operator()() const {
return this->value_;
}
T& operator()(){
return this->value_;
}
Number<T> operator+(const Number<T>& other){
Number<T> ret( this->value_ + other() );
return ret;
}
template<typename U>
Number<T> operator+(const Number<U>& other){
return Number<T>( this->value_ + static_cast<T>(other()));
}
protected:
T value_;
};
Number<int> c(42);
std::cout << "a+b = " << (a+b)() << std::endl;
std::cout << "a+c = " << (a+c)() << std::endl;
std::cout << "c+a = " << (c+a)() << std::endl;

Une classe Matrice

Allons plus loin

Quelques utilisations avancées des templates

CRTP

Idée générale

Singleton

template <typename Base>
class Singleton {
public:
static Base& GetInstance() {
if ( Singleton<Base>::internal_ == nullptr){
Singleton<Base>::internal_ = std::make_shared<Base>();
}
}
Singleton(Singleton const &) = delete;
Singleton &operator=(Singleton const &) = delete;
protected:
static std::shared_ptr<Base> internal_ ;
Singleton(){};
};
template<typename Base>
std::shared_ptr<Base> Singleton<Base>::internal_ {nullptr};
class A : public Singleton<A> {
// Rest of functionality for class A
public:
void print() const {
std::cout << "address(A) = " << this << std::endl;
}
protected:
};
int main(){
auto& a = A::GetInstance();
a.print();
auto& b = A::GetInstance();
b.print();
return EXIT_SUCCESS;
}

Composition

template<typename Format>
class Logger: public Format{
public:
void message( const std::string& msg){
std::cout << this->prefix() << msg << this->suffix() << std::endl;
}
};
class Color {
protected:
std::string prefix(){
return "\e[1;31m";
}
std::string suffix(){
return "\e[0m";
}
};
class Italic {
protected:
std::string prefix(){
return "\e[3m";
}
std::string suffix(){
return "\e[0m";
}
};
int main(){
Logger<Color> log_color;
log_color.message("coucou");
Logger<Italic> log_italic;
log_italic.message("coucou");
return EXIT_SUCCESS;
}

Polymorphisme statique

template<typename Derived>
class Base{
public:
void doSomething(){
static_cast<Derived*>(this)->doSomethingSpecial();
}
};
class DerivedA: public Base<DerivedA>{
public:
std::cout << "In DerivedA::doSomethingSpecial" << std::endl;
}
};
class DerivedB: public Base<DerivedB>{
public:
std::cout << "In DerivedB::doSomethingSpecial" << std::endl;
}
};
int main(){
return EXIT_SUCCESS;
}

Variadic templates