====== Les classes génériques en C++ ======
===== Pourquoi des classes génériques en C++ =====
Supposons que nous souhaitons définir une classe qui founit un buffer d'une taille prédéfinie :
#include
class buffer
{
private:
uint8_t m_buffer[128];
public:
buffer(const buffer& anotherBuffer)
{
std::copy(anotherBuffer.m_buffer, anotherBuffer.m_buffer + 128, m_buffer);
}
int size() const { return 128; }
buffer& operator = (const buffer& anotherBuffer)
{
std::copy(anotherBuffer.m_buffer, anotherBuffer.m_buffer + 128, m_buffer);
return this;
}
operator const uint8_t*() const { return m_buffer; }
operator uint8_t*() { return m_buffer; }
uint8_t operator[](int anIndex) const
{
if(anIndex >= 128)
throw std::out_of_range("index larger than buffer");
return m_buffer[anIndex];
}
uint8_t& operator[](int anIndex)
{
if(anIndex >= 128)
throw std::out_of_range("index larger than buffer");
return m_buffer[anIndex];
}
};
Cette classe définit un buffer d'une taille de 128 octets. Elle redéfinit les opérateurs suivant :
* ''operator[]'' qui permet d'accéder en lecture et en écriture au buffer interne natif.
* ''operator uint8_t*()'' qui permet de récupérer un pointeur autorisant la lecture ou l'écriture dans le buffer vu.
* ''operator =(const buffer&)'' et ''buffer(const buffer&)'' correspondant à l'opérateur de recopie.
Nous voyons que si nous souhaitons faire un buffer d'une taille différente, nous devons définir une nouvelle classe dont la seule différence est la taille du buffer.
De même si nous souhaitons faire un buffer qui va stocker des éléments de base autres que des entiers 8 bits, par exemple des entiers 64 bits, il est nécessaire de réécrire une nouvelle classe en remplaçant ''uint8_t'' par un type différent.
===== Paramétriser une classe par un type =====
Dans un premier temps, nous allons paramétriser la classe ''buffer'' par le type des éléments qui sont contenus dans le ''buffer''.
Pour ce faire, nous allons déclarer la classe ''buffer'' comme étant un modéle de classe à l'instar des [[cpp:syntax:functions:template|modèles de fonctions]].
#include
template
class buffer
{
private:
itemT m_buffer[128];
public:
buffer(const buffer& anotherBuffer)
{
std::copy(anotherBuffer.m_buffer, anotherBuffer.m_buffer + 128, m_buffer);
}
int size() const { return 128; }
buffer& operator = (const buffer& anotherBuffer)
{
std::copy(anotherBuffer.m_buffer, anotherBuffer.m_buffer + 128, m_buffer);
return this;
}
operator const itemT*() const { return m_buffer; }
operator itemT*() { return m_buffer; }
itemT operator[](int anIndex) const
{
if(anIndex >= 128)
throw std::out_of_range("index larger than buffer");
return m_buffer[anIndex];
}
itemT& operator[](int anIndex)
{
if(anIndex >= 128)
throw std::out_of_range("index larger than buffer");
return m_buffer[anIndex];
}
};
La déclaration de la classe comme un modèle de classe s'effectue par :
template
class buffer
qui indique que la classe est un __template__ qui a un type paramètre qui est le type des éléments stockés dans le buffer, soit le type ''itemT''.
Désormais, pour utiliser la classe ''buffer'', il est nécessaire de spécifier le type de ses éléments. Ainsi, quand je souhaite créer un buffer de caractères, il suffit d'écrire ''buffer'':
void funct()
{
buffer bufferOfCharacters;
bufferOfCharacters[0] = 't';
bufferOfCharacters[1] = 'e';
bufferOfCharacters[2] = 's';
bufferOfCharacters[3] = 't';
std::string token((const char*)bufferOfCharacters, (const char*)bufferOfCharacters + 4);
std::cout << token << std::endl;
}
Cette fonction crée un buffer de 128 caractères dénommé ''bufferOfCharacters'', stocke dans le buffer les caractères 't', 'e', 's', 't', copie ces 4 caractères dans un objet ''string'' et affiche le contenu de l'objet ''string'' sur la console.
Il est possible de paramétrer des classes avec plus d'un type. Par exemple, la déclaration de la classe [[https://en.cppreference.com/w/cpp/string/basic_string|std::basic_string]] de la STL définit trois paramètres de type ''charT'', ''traits'' et ''allocator'' comme suit :
template<
class charT,
class traits = std::char_traits,
class allocator = std::allocator
class basic_string;
Il est intéressant de constater qu'il est possible de définir un paramètre de type par défaut. Ainsi, écrire :
std::basic_string myString;
est équivalent à écrire :
std::basic_string> myString;
Si nous ne spécifions pas les types ''traits'' et ''allocator'', le compilateur utilise les types par défaut.
===== Paramétriser une classe par une valeur =====
Nous souhaitons pouvoir choisir la taille du buffer. Pour ce faire, nous allons définir un modèle de classe qui est paramétrisé par la taille que nous souhaitons allouer au buffer.
#include
template
class buffer
{
private:
itemT m_buffer[bufferSize];
public:
buffer(const buffer& anotherBuffer)
{
std::copy(anotherBuffer.m_buffer, anotherBuffer.m_buffer + bufferSize, m_buffer);
}
int size() const { return bufferSize; }
buffer& operator = (const buffer& anotherBuffer)
{
std::copy(anotherBuffer.m_buffer, anotherBuffer.m_buffer + bufferSize, m_buffer);
return this;
}
operator const itemT*() const { return m_buffer; }
operator itemT*() { return m_buffer; }
itemT operator[](int anIndex) const
{
if(anIndex >= bufferSize)
throw std::out_of_range("index larger than buffer");
return m_buffer[anIndex];
}
itemT& operator[](int anIndex)
{
if(anIndex >= bufferSize)
throw std::out_of_range("index larger than buffer");
return m_buffer[anIndex];
}
};
Désormais, quand nous créons un buffer, nous pouvons spécifier la taille du buffer.
buffer bufferOfIntegers; // buffer contenant 64 entiers.
buffer bufferOfChars; // buffer contenant 32 caractères.
buffer bufferOfFloats; // buffer contenant 128 (la valeur par défaut) nombres à virgule flottante.
===== Pour aller plus loin =====
[[https://en.cppreference.com/w/cpp/language/class_template|Template class]]
[[https://en.cppreference.com/w/cpp/language/template_parameters|Template parameters and template arguments]]
[[https://en.cppreference.com/w/cpp/language/template_argument_deduction|Template argument deduction]]
[[https://en.cppreference.com/w/cpp/language/constraints|Contraints & Concepts]]