L'approche par injection de parents est fortement similaire à la stratégie collaborative et générique, reposant sur les mêmes concepts mais exprimés de manière différente.
Imaginons un type de « Croisseur » ici (nom discutable, mais il s'agit d'un nom trouvé tard en fin de soirée et les suggestions pour mieux le nommer sont les bienvenues) capable d'exprimer certaines caractéristiques propres à un outil susceptible de prendre en charge une stratégie de croissance. On voudra qu'un « Croisseur » ne soit pas polymorphique, mais définisse certains types autour des concepts de valeur (T) et de capacité (SzT), dans le but d'alléger la syntaxe de sa méthode grow().
Chaque type « Croisseur » (par exemple CroisseurTypique) pourra déterminer une stratégie de croissance qui lui est propre. Aucun polymorphisme ne sera requis; on exigera en retour une certaine conformité sur le plan de la signature de la méthode grow() des divers types de « Croisseurs ».
Un tableau dynamique sera un cas particulier (par héritage privé!) de « Croisseur », quel qu'il soit mais déterminé à la compilation pour chaque instance de tableau dynamique. Ce parent injecté par le code client prendra en charge la croissance de son enfant. Le « Croisseur » en question sera d'un type déterminé par les paramètres d'instanciation du template Tableau, ce qui fait que chaque instance de Tableau sera susceptible d'avoir sa propre stratégie de croissance. Chaque Tableau reposera sur un type de valeur (le type T) et sur un type de « Croisseur », et cette combinaison entraînera à la fois la génération de l'instance elle-même et, au besoin, de son type.
Cette stratégie est très souple, mais moins que la stratégie collaborative polymorphique : les concepts de tableau et de politique de croissance sont découplés, réduits à leur plus simple expression (celle de service de croissance sans état), et la politique de croissance peut changer d'une instance de Tableau à l'autre, mais ne pourra pas changer pour un Tableau donné au cours de son existence. Puisque aucun pointeur vers un « Croisseur » n'est impliqué, le nombre d'indirections est fortement réduit et le code sera en général beaucoup plus rapide du fait qu'il sera sujet à une plus grande quantité d'optimisations de la part du compilateur.
Le couplage entre deux classes est plus fort en situation d'héritage qu'en situation de composition. Cependant, dans ce cas-ci, l'héritage est un détail d'implémentation de l'enfant puisqu'il s'agit d'héritage privé, à l'insu du code client. Ceci permet au parent d'exposer ses services par une méthode protégée, utile seulement pour l'enfant. Voir https://wandbox.org/permlink/s4f3CMnyiqQPpVau :
Remarquez l'implémentation de grow() dans Tableau : elle invoque GrowthPol::grow() avec des paramètres. Pourquoi n'invoque-t-elle pas plutôt simplement grow() avec des paramètres? Après tout, les signatures des méthodes grow() de l'enfant et du parent sont différentes, non?
#ifndef TABLEAU_H
#define TABLEAU_H
#include <algorithm>
#include <utility>
#include <initializer_list>
class HorsBornes {};
template <class T, class SzT = std::size_t>
struct CroisseurTypique {
using value_type = T;
using pointer = T*;
using size_type = SzT;
protected:
std::pair<pointer, size_type> grow(pointer old_p, size_type old_cap) const {
using std::copy;
const size_type TAILLE_DEFAUT = 16;
const float FACTEUR_CROISSANCE = 1.5f;
const auto nouv_cap = static_cast<size_type>(old_cap? old_cap * FACTEUR_CROISSANCE : TAILLE_DEFAUT);
pointer p = new value_type[nouv_cap];
try {
copy(old_p, old_p+old_cap, p);
} catch(...) {
delete [] p;
throw;
}
return { p, nouv_cap };
}
};
template <class T, class GrowthPol = CroisseurTypique<T, int>>
class Tableau : GrowthPol { // héritage privé
public:
// using ...
private:
pointer elems {};
size_type nelems {},
cap {};
public:
// empty(), size(), capacity(), full()
// begin(), cbegin(), end(), cend()
// constructeur par défaut
// constructeur paramétrique
//
// constructeur de copie
//
Tableau(const Tableau &autre)
: GrowthPol{ autre }, nelems{ autre.size() }, elems{ new value_type[autre.size()] }, cap{ autre.size() } {
try {
std::copy(autre.begin(), autre.end(), begin());
} catch(...) {
delete [] elems;
throw;
}
}
// constructeur par liste d'initialisation
// constructeur de séquence
// constructeur de conversion
// constructeur de mouvement
// constructeur de mouvement
// destructeur, swap(), affectation, affectation covariante
// affectation par mouvement
private:
void grow() {
auto [nouv_p, nouv_cap] = GrowthPol::grow(elems, capacity());
delete [] elems;
elems = nouv_p;
cap = nouv_cap;
}
public:
// push_back(), at(), operator[]
// operator==, operator!=
};
#endif
Un programme de test suit. Ce programme ajoute N éléments au tableau, provoquant au passsage à plus d'une reprise l'activation de la stratégie de croissance, puis affiche les éléments sur la sortie standard.
#include "Tableau.h"
#include <iostream>
int main() {
using namespace std;
enum { N = 1'000 };
Tableau<int> tab;
for (int i = 0; i < N; ++i)
tab.push_back(i);
for(auto n : tab) cout << n << ' ';
cout << endl;
}
Que pensez-vous cette solution?