Rechercher
 

Définition de classes templates en C++

Une classe générique (une classe template) est en fait un ensemble de modèles de code permettant de produire des classes, et leurs méthodes, pour divers types de données. Tant que ces modèles de code ne sont pas utilisé dans votre programme, aucun code (relatifs à ces templates) ne sera produit dans l'exécutable (ou la librairie) résultant de la compilation. Par contre à chaque nouvelle utilisation de ces templates, et en fonction des types passés en paramètres de ces templates, le compilateur pourra produire les classes et les méthodes requises.

Attention, tout comme pour une fonction template et comme vos classes et méthodes sont génériques, vous ne devez pas faire de présupposition sur les types de données qui seront utilisés par ces dernières : il est donc important de réfléchir dans le pire des cas. Par exemple, vous ne connaissez pas à l'avance la taille des types de données qui seront utilisés. Il en résulte qu'il est important de passer les paramètres de méthodes par références : ainsi on limitera le nombre d'octets qui seront empiler sur la pile d'exécution (la stack) durant l'appel.

Le « Hello World » de la classe template consiste à définir une classe générique de tableaux à taille fixe. Effectivement, et en préambule, n'oubliez jamais qu'un tableau à la C pose un certain nombre de problèmes : le principal étant que lorsque l'on y accède, aucune vérification de dépassement des bornes du tableau n'est réalisé. Il peut donc intéressant de recoder une classe réalisant des contrôles lors des accès. Un autre problème réside dans le fait que si votre tablau est alloué dynamiquement (via un opérateur new) alors on ne respecte pas la phylosophie RAII (Resource Aquisition Is Initialization) chère aux développeurs C++ « modernes » (pour faire court, la libération du tableau reste alors de la responsabilité du développeur). Le fait de coder une classe générique devrait aussi permettre de régler ce problème. Pour en revenir à notre problématique principale, notons que l'aspect générique se prête bien à notre cas d'école, car les algorithmes utilisés par une classe de tableaux sont strictement indépendant de la nature des éléments stockés dans ces tableaux.

Note : il est important de comprendre que cet exemple vous est proposé à titre d'introduction au concept de classe générique. Dans la vrai vie il est strictement inutile de recoder cette classe pour la simple et bonne raison qu'elle est proposé par défaut par C++11. Plus tard, utilisez donc plutôt l'implémentation standard : la classe générique s'appelle elle aussi std::array (mais avec une minuscule) et elle est proposée par l'entête standard <array>.

#ifndef __ARRAY_H__
#define __ARRAY_H__

#include <cstddef>
#include <iostream>
#include <sstream>
#include <stdexcept>

template <typename ELEMENT>
class Array {
    ELEMENT * rawArray;
    size_t size;
public:
    
    Array( size_t size = 10 );
    Array( const Array<ELEMENT> & original );
    ~Array();

    size_t getSize() const;

    Array<ELEMENT> & operator=(  const Array<ELEMENT> & original );
    ELEMENT & operator[]( size_t position ) const;
};

template <typename ELEMENT>
std::ostream & operator<<( std::ostream & os, const Array<ELEMENT> & array );

// --- Implementations --- 

template <typename ELEMENT>
Array<ELEMENT>::Array( size_t size ) : size(size), rawArray( new ELEMENT[size] ) {
}

template <typename ELEMENT>
Array<ELEMENT>::Array( const Array<ELEMENT> & original ) {
    this->rawArray = nullptr;
    *this = original;
}

template <typename ELEMENT>
Array<ELEMENT>::~Array() {
    delete [] this->rawArray;
}

template <typename ELEMENT>
size_t Array<ELEMENT>::getSize() const {
    return this->size;
}

template <typename ELEMENT>
Array<ELEMENT> & Array<ELEMENT>::operator=( const Array<ELEMENT> & original ) {
    if ( this->rawArray != nullptr ) delete [] this->rawArray;
    this->size = original.size;
    this->rawArray = new ELEMENT[ this->size ];
    for( size_t position=0; position<this->size; ++position ) {
        this->rawArray[ position ] = original.rawArray[ position ];
    }

    return *this;
}

template <typename ELEMENT>
ELEMENT & Array<ELEMENT>::operator[]( size_t position ) const {
    if ( position >= this->size ) {
        std::stringstream buffer;
        buffer << "Bad position " << position;
        throw std::out_of_range( buffer.str() ); 
    }
    return this->rawArray[ position ];
}


template <typename ELEMENT>
std::ostream & operator<<( std::ostream & os, const Array<ELEMENT> & array ) {
    os << "[";
    size_t size = array.getSize();
    for( size_t position=0; position<size; ++position ) {
        os << array[position];
        if ( position < size-1 ) os << ", ";
    }
    return os << "]";
}

#endif

Notez que dans l'exemple ci-dessus, les méthodes sont implémentées, certes dans le .h, mais en dehors de la classe. Cela permet de conserver la séparation classique entre la déclaration et l'implémentation d'une méthode. Néanmoins, certains développeurs préfèrent implémenter les méthodes génériques directement dans le corps de la classe : c'est possible, comme le montre l'exemple ci-dessous. Attention, dans ce cas, les méthodes produites seront aussi considérées comme inline.

#ifndef __ARRAY_H__
#define __ARRAY_H__

#include <cstddef>
#include <iostream>
#include <sstream>
#include <stdexcept>

template <typename ELEMENT>
class Array {
    ELEMENT * rawArray;
    size_t size;
public:
    
    Array( size_t size = 10 ): size(size), rawArray( new ELEMENT[size] ) {
    }
    
    Array( const Array<ELEMENT> & original ) {
        this->rawArray = nullptr;
        *this = original;
    }

    ~Array()  {
        delete [] this->rawArray;
    }

    size_t getSize() const {
        return this->size;
    }

    Array<ELEMENT> & operator=( const Array<ELEMENT> & original ) {
        if ( this->rawArray != nullptr ) delete [] this->rawArray;
        this->size = original.size;
        this->rawArray = new ELEMENT[ this->size ];
        for( size_t position=0; position<this->size; ++position ) {
            this->rawArray[position] = original.rawArray[position];
        }

        return *this;
    }

    ELEMENT & operator[]( size_t position ) const {
        if ( position >= this->size ) {
            std::stringstream buffer;
            buffer << "Bad position " << position;
            throw std::out_of_range( buffer.str() ); 
        }
        return this->rawArray[ position ];
    }
};

template <typename ELEMENT>
std::ostream & operator<<( std::ostream & os, const Array<ELEMENT> & array ) {
    os << "[";
    size_t size = array.getSize();
    for( size_t position=0; position<size; ++position ) {
        os << array[position];
        if ( position < size-1 ) os << ", ";
    }
    return os << "]";
}

#endif

Il nous faut maintenant écrire un code de test pour vérifier le bon fonctionnement de notre classe. Je vous laisse le choix de la version de la classe (parmis les deux vue ci-dessus) à utiliser dans cet exemple.

#include <iostream>
#include <string>

#include "Array.h"

using namespace std;


int main( int argc, char * argv[] ) {

    // An array of strings
    Array<string> strArray( 3 );
    strArray[0] = "begin";
    strArray[1] = "middle";
    strArray[2] = "end";
    cout << strArray << endl; 
 
    // A copy of first array
    Array<string> copy = strArray;
    copy[1] = "other";
    cout << copy << endl;
 
    // An array of integers
    Array<int> intArray( 5 );
    size_t position = 0;
    try {
        while( true ) {
            intArray[ position++ ] = position * 10;
        }
    } catch( out_of_range & e ) {
        cout << e.what() << endl;
    }

    cout << intArray << endl;

    
    return 0;
}

Vous l'avez peut être remarqué, le codage de la classe Array présuppose que vous utilisiez un compilateur compatible avec le standard C++11 (utilisation de nullprt). Pour les utilisateurs de g++, exécutez la ligne de commande suivante : g++ -std=c++11 -o Sample Sample.cpp. Pour tout autre compilateur, vérifiez qu'il soit compatible C++11 au niveau de sa documentation.