Salut,
Tu devrais en réalité beaucoup plus séparer les différentes notions dont tu as besoin, entre autres, en utilisant ce que l'on appelle des traits et des politiques.
J'ai récemment expliqué exactement ce qu'étaient ces deux drôles de bêtes à l'occasion d'une autre discussion, et je ne vais donc pas recommencer ici 
Mais je vais essayer de te donner une idée de la manière de t'en sortir dans le cas présent.
La première chose dont tu dois prendre conscience, c'est que le principe de la programmation générique est simple et qu'il se résume à une prhrase:
si on ne sait pas exactement
quel type de donnée nous allons manipuler, nous savons en revanche parfaitement
comment nous allons les manipuler
Aussi simple que soit ce principe, je le trouve génial, parce qu'il met bien en évidence le fait que nous travaillons toujours avec deux notions parfaitement distinctes, et pourtant foncièrement dépendantes l'une de l'autre. Je parle bien sur des notions de comportements et de types de données.
En effet, n'importe quelle application ne poursuit jamais qu'un seul et même objectif : celui de manipuler des données. Et lorsque l'on manipule des données, nous ne pouvons faire appel qu'à des comportements qui sont autorisés par le type de la donnée que nous sommes en train de manipuler.
Rien n'empêche de prévoir un comportement "accélérer" ou "estBissextile" pour manipuler une donnée d'un type bien particulier. A condition toutefois que ce comportement ait du sens pour le type de la donnée que l'on manpule: prévoir le comportement accélérer pour la notion de date ou estBissextile pour la notion de véhicule n'aurait absolument aucun sens!
Mais, d'un autre coté, certains types de données qui n'ont rien à voir les uns avec les autres peuvent parfaitement supporter des comportements identique: un type de donnée qui correspond à la notion de "collection de donnée" devra nous permettre d'utiliser des comportement comme "ajouter", "retirer" ou encore "parcourir" et les types de données "numériques" nous permettront d'utiliser des comportement comme "additionner", "soustraire" ou "multiplier".
Là où le bât blesse, c'est dans le fait que deux types de donnée différents peuvent accepter des comportements qui seront désignés de la même manière (sous le même nom, pourrions nous dire), alors que les données manipulées (et la manière de les manipuler pour obtenir le résultat souhaité) sont totalement différentes. Nous devons donc mettre en place des "adaptateurs", des "passerelles" qui nous permettront de manipuler de manière "standardisées" des données qui n'ont aucun lien entre elles.
Tu es parti sur une structure nommée RGB. Mais ca, c'est déjà une structure particulièrement précise, qui te permet d'utiliser des comportements très spécifiques et qui est ... trop précise que pour nécessiter le recours à la généricité.
Par contre, cette structure fait partie d'un ensemble que l'on pourrait appeler color_scheme ("schéma de couleur"), au même titre que bien d'autre schémas de couleurs que l'on pourrait utiliser. Et il peut -- effectivement -- être sympa de se donner la possibilité de manipuler de manière "standardisée" ... n'importe quel schéma de couleur.
Pour cela, la première chose que l'on va devoir faire, c'est de "standardiser" la manière dont on va ... décrire les schéma de couleur
. Par facilité, nous pouvons partir de la structure RGB pour voir comment nous voudrions la décrire (nous verrons après si cette description "tient la route" pour les autres schémas de couleurs
) :
La structure RGB permet de représenter une couleur complexe qui est la combinaison de trois valeurs de base (respectivement le rouge, le rert et le bleu). Ces trois valeurs de base sont -- a priori -- des valeurs numériques, entières, non signées, dont chacune est représentée par un byte (huit bits).
Seulement, cette description est tout sauf générique! Voyons voir si nous pouvons la rendre plus générique:
un schéma de couleur permet de représenter une couleur complexe qui est la combinaison "d'un certain nombre" de valeurs de base. ces "valeurs de base" sont des valeurs numérique dont le type est identique pour l'ensemble des valeurs de base qui doivent être représentées
Pour rendre cette description générique plus complète, nous pouvons ajouter que
pour un schéma de couleur donné, les différentes couleurs de base doivent être représentées dans un ordre bien particulier, spécifique au schéma de couleur envisagé
et, même si on le sait, que
l'intervalle des valeurs admissibles pour un schéma de couleur donné dépend -- le plus souvent -- du type de données utilisé pour représenter chacune des couleurs "sous-jacentes"
Voilà donc -- en termes français -- la manière dont nous voudrions pouvoir définir "n'importe quel schéma de couleur". Il ne reste "plus qu'à" faire en sorte de le faire comprendre au compilateur 
Pour cela, nous allons travailler "par étapes" et introduire les notions l'une après l'autre.
La notion la plus "simple" que nous ayons définie est la notion d'intervalle, qui est -- malgré tout -- déjà composée d'un type de donnée et de deux valeurs spécifiques (minimum et maximum). Nous pouvons donc la représenter sous une forme proche de
1 2 3 4 5 6 7 8
| template <typename T>
struct intervalle_trait{
using intervalle_type = T;
/* définissons la valeur minimale admissible */
static constexpr intervalle_type min = std::numeric_limits<intervalle_type>::min();
/* définissons la valeur maximale admissible */
static constexpr intervalle_type max = std::numeric_limits<intervalle_type>::max();
} |
Oui, mais non! Parce que cette manière de représenter le minimum et le maximum ne laisse pas le choix au développeur de définir ses propres valeurs minimales et maximale
Modifions la donc un peu pour lui donner cette occasion en cas de besoin:
1 2 3 4 5 6 7 8 9 10
| template <typename T,
T MIN=std::numeric_limits<T>::min(),
T MAX = std::numeric_limits<T>::max()>
struct intervalle_trait{
using intervalle_type = T;
/* définissons la valeur minimale admissible */
static constexpr intervalle_type min = MIN;
/* définissons la valeur maximale admissible */
static constexpr intervalle_type max = MAX;
} |
Ensuite, nous devons pouvoir faire comprendre au compilateur comment nous allons nous y prendre pour décrire le fait qu'il faut "un certain nombre" de "couleurs sous-jacentes", qui sont toutes "d'un type de donnée particulier". Cela pourrait se faire sous une forme proche de
1 2 3 4 5 6
| template <typename T, size_t SIZE>
struct internal_trait{
using external_type = T;
static constexpresion size_t size = SIZE;
using collection_type = std::array<internal_type, size>;
}; |
Avant que je ne l'oublie!!! L'un des comportements essentiel dont on a besoin avec la notion de schéma de couleur, c'est -- très certainement -- de pouvoir accéder à n'importe quelle couleur sous-jacente (sous une forme constante et sous une forme non constante). Les deux traits de politiques que j'ai déjà définis nous permettent de fournir ce comportement de manière assez aisée. Observe un peu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| template
<typename T, size_t SIZE>
struct subcolor_getter{
/* par facilité, je reprend les types que j'ai déjà créés */
using color_type = internal_trait<T, SIZE>;
using sub_type = typename color_type:: internal_type;
using colleciton_type = typename color_type::collection_type;
static constexpr size_t maxIndex = SIZE;
static constexpr size_t bitDepth = sizeof(T) * CHAR_BIT;
/* la version constante du comportement */
static color_type get(collection_type const & coll, size_t pos){
assert(pos < maxIndex && "index out of bound");
return coll[pos];
}
/* la version constante du comportement */
static color_type& get(collection_type & coll, size_t pos){
assert(pos < maxIndex && "index out of bound");
return coll[pos];
}
}; |
Et plus important encore, il faut que j'ai la possibilité de créer une couleur sur base d'un schéma de couleur donné. Pour ce faire, je dois envisager deux possibilités:
- soit je crée une couleur pour laquelle toutes les "données sous-jacentes" sont initialisées à la valeur par défaut pour le type de données qui les représente,
- soit je crée une couleur pour laquelle chacune des "données sous-jacentes" sont initialisées à une valeur (fournie par l'utilisateur) qui lui est propre.
En un mot, l'utilisateur doit avoir le choix entre ne fournir aucune valeur, ou fournir toutes les valeurs pour créer sa couleur: Si il veut créer une couleur RGB, qui contient donc ... trois couleurs, il aura le choix entre fournir 0, 1 ou... 3 valeurs pour le faire. Il est totalement hors de question qu'il puisse le faire en ne fournissant que deux valeurs!
Et cela sous-entend que le compilateur ne pourra surtout pas accepter de créer une couleur si on lui donne un nombre différent de valeurs.
Pour décrire cela nous allons définir une politique qui prendra la forme de
1 2 3 4 5 6 7 8 9 10 11
| template <typename T, size_t SIZE, size_t COUNT>
struct color_creator{
using color_type = internal_trait<T, SIZE>;
using sub_type = typename color_type:: internal_type;
using colleciton_type = typename color_type::collection_type;
template <typename ... Args>
static collection_type create(Args ... args){
static_assert(false, "bad parameter count");
}
}; |
pour laquelle nous fournirons deux spécialisations partielles sous la forme de
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template <typename T, size_t SIZE>
struct color_creator<T, SIZE, SIZE>{
template <typename ... Args>
static collection_type create(Args ... args){
return collection_type{args}...;
}
};
/* ET */
template <typename T, size_t SIZE>
struct color_creator<T, SIZE, 0>{
static collection_type create(){
return collection_type{};
}
}; |
Grace à cela nous pourrons indiquer au compilateur la manière dont on veux définir nos schémas de couleur, ce qui prendra une forme proche de
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
template <typename T, size_t SIZE,
size_t MIN= std::numeric_limits<T>::min(), size_t MAX= std::numeric_limits<T>::max()>
class color_scheme{
public:
/* quelques notions qu'on peut exposer */
using intervalle_type = intervalle_trait<T, MIN, MAX>;
/* si on prévoit de vérifier les intervalles, nous pourrons le faire */
static constexpr intervalle_min = intervalle_type::min;
static constexpr intervalle_max = intervalle_type::max;
using internal_type= internal_trait<T, SIZE>;
using collection_type = internal_type::collection_type;
static constexpr MAXSIZE = SIZE;
using value_type = internal_type::external_type;
color_scheme():datas_{color_creator<MAXSIZE, 0>::create()}{
}
template <typename ... Args>
color_scheme(Args ... args):datas_{color_creator<MAXSIZE, sizeof ...(Args)>::create(args...)}{
}
value_type & operator[](size_t index){
return subcolor_getter<T, SIZE>::get(datas_);
}
value_type const & operator[](size_t index) const{
return subcolor_getter<T, SIZE>::get(datas_);
}
private:
collection_type datas_;
}; |
Et, grâce à cette description, tu pourras définir ton schéma de couleur RGB sous une forme proche de
using RGB_SCHEME = color_scheme<unsigned char, 3>;
et ce schéma de couleur serait -- par nature -- totalement différent d'un schéma de couleur proche de
using SOMEOTHER_SCHEME = color_scheme<unsigned char, 3, 0,4>;
Il y aurait cependant un dernier problème: certains schémas de couleur ne diffèrent entre eux que par l'ordre dans lequel les différentes couleurs sous-jacentes sont représentées; si bien que le compilateur considérera que les alias de type
using RGBA_SCHEME = color_schme<unsigned_char, 4>;
et
using AGBR_SCHEME = color_schme<unsigned_char, 4>;
sont parfaitement équivalent et correspondent au final au même type de données (ce qui permettrait de "mélanger" les couleurs créées sur base de ces deux schémas de couleur) 
Évidemment, cela ne ferait pas notre affaire, car on se retrouverait assez facilement à considérer la valeur assignée au rouge comme étant celle assignée au canal alpha (ou inversement). Nous devons donc prévoir un moyen (optionnel) de faire la distinction entre les deux. Et ce moyen pourrait passer par l'existence d'un tag.
Pour pouvoir rendre la présence de ce tag optionnelle, nous devrons donc créer un tag qui décrit spécifiquement le fait qu'il n'y a aucune ambiguité à craindre, par exemple sous la forme de et nous pourrons alors l'intégrer à notre schéma de couleurs sous une forme proche de
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
template <typename T, size_t SIZE, typename TAG = NonAmbiguteTag,
size_t MIN= std::numeric_limits<T>::min(), size_t MAX= std::numeric_limits<T>::max()>
class color_scheme{
/* ces informations sont d'ordre privées ;) */
using intervalle_type = intervalle_trait<T, MIN, MAX>;
/* si on prévoit de vérifier les intervalles, nous pourrons le faire */
static constexpr intervalle_min = intervalle_type::min;
static constexpr intervalle_max = intervalle_type::max;
using internal_type= internal_trait<T, SIZE>;
using collection_type = internal_type::collection_type;
static constexpr MAXSIZE = SIZE;
public:
/* quelques notions qu'on peut exposer */
using value_type = internal_type::external_type;
color_scheme():datas_{color_creator<MAXSIZE, 0>::create()}{
}
template <typename ... Args>
color_scheme(Args ... args):datas_{color_creator<MAXSIZE, sizeof ...(Args)>::create(args...)}{
}
value_type & operator[](size_t index){
return subcolor_getter<T, SIZE>::get(datas_);
}
value_type const & operator[](size_t index) const{
return subcolor_getter<T, SIZE>::get(datas_);
}
private:
collection_type datas_;
}; |
Ainsi, si on veut faire la différence entre RGBA et AGBR, nous pourrons créer deux tag (un pour chaque type de donnée spécifique) proche de
1 2
| struct RgbaTag{};
struct AgbrTag{}; |
et rendre les deux alias de type totalement différents grâce à eux, sous les formes de
using RGBA_SCHEME = color_schme<unsigned_char, 4, RgbaTag>;
et de
using AGBR_SCHEME = color_schme<unsigned_char, 4, AgbrTag>;
NOTA: tout le code écrit dans cette intervention a été écrit "à la volée" et n'a en aucun cas été testé. Quelques erreurs peuvent donc exister dedans
Partager