OVH Cloud OVH Cloud

[LONG] template et operator[]

5 réponses
Avatar
Vincent Richard
Bonjour,

Je dispose d'une classe de /smart pointer/ et je cherche à implanter
l'opérateur [] dessus, et déléguer l'appel à l'objet encapsulé pour
des raisons de confort d'utilisation :

template <typename T>
class ref // très simplifié...
{
public:

ref(T* ptr) : m_ptr(ptr) { }
~ref() { delete m_ptr; }

T& operator*() { return *m_ptr; }
T* operator->() { return m_ptr; }

template <typename X, typename Y>
Y operator[](const X& p) const
{
return (*m_ptr)[p];
}

private:

T* m_ptr;
};

struct obj
{
const int operator[](const double d) const
{
return static_cast <int>(d * d); // pour l'exemple...
}
};

int main()
{
ref <obj> x = new obj();

const int foo = x[2.0]; // ligne 39

// ==> je veux éviter de faire (*x)[2.0]...
}

J'utilise g++ v4.0.1, la compilation produit l'erreur suivante :

plop.cpp: In function ‘int main()’:
plop.cpp:39: error: no match for ‘operator[]’ in ‘x[2.0e+0]’

Il semblerait que le compilateur ne parvienne pas à déduire les
types de l'expression (ça me paraît peu probable quand même).

J'ai tenté d'ajouter un objet intermédiaire :

template <typename T, typename X>
class proxy
{
public:

proxy(T& ref, const X& prm) : m_ref(ref), m_prm(prm) { }

template <typename Y>
operator Y() const
{
return m_ref[m_prm];
}

private:

T& m_ref;
X m_prm;
};

et modifié ref::operator[] comme ceci :

template <typename X>
proxy <T, X> operator[](const X& p) const
{
return proxy <T, X>(*m_ptr, p);
}

même si ça fait un peu "bidouille", le code compile et fonctionne
correctement.

Seulement maintenant, j'ai un autre problème. Il faudrait également que
je puisse faire quelque chose comme :

std::cout << x[2.0];

et là, j'obtiens tout un tas d'erreur concernant un "ambiguous
overload", ce qui est normal car ostream::operator<<() accepte
tout un tas de types.

Pourtant... il n'y a qu'une seule version de obj::operator[] et pas de
conversion implicite possible (a priori). Pourquoi le compilateur
ne parvient-il pas à résoudre l'ambiguïté ?

Comment résoudre ce problème ?
Et déjà, est-ce faisable au moins ? :-)

Merci d'avance !

Vincent

5 réponses

Avatar
Cyrille

template <typename X, typename Y>
Y operator[](const X& p) const
{
return (*m_ptr)[p];
}


En effet ici, le compilateur n'a aucun moyen de résoudre Y. Je ne
connais pas trop les détails du pouquoi du comment, mais il y a
certainement ici quelqu'un pour t'éclairer.

Seulement maintenant, j'ai un autre problème. Il faudrait également que
je puisse faire quelque chose comme :

std::cout << x[2.0];

et là, j'obtiens tout un tas d'erreur concernant un "ambiguous
overload", ce qui est normal car ostream::operator<<() accepte
tout un tas de types.

Pourtant... il n'y a qu'une seule version de obj::operator[] et pas de
conversion implicite possible (a priori). Pourquoi le compilateur
ne parvient-il pas à résoudre l'ambiguïté ?


Comment veux-tu qu'il sache quel est le bon type qu'il doit utiliser
pour instancier l'opérateur de conversion du proxy? Quand tu l'affecte à
un int, ok. Mais sinon?

Comment résoudre ce problème ?
Et déjà, est-ce faisable au moins ? :-)


Si tu peux modifier ta classe obj, tu peux y ajouter:

struct obj
{
typedef int return_type;
// ....
};

Et alors l'opérateur [] dans ref s'écrit ainsi:
template<typename X>
X::return_type operator[](const X& p) const
{
return (*m_ptr)[p];
}

Si tu ne peux pas modifier obj, tu utilises une méta-fonction:

template<typename T>
struct my_return_type
{
};

template<>
struct my_return_type<obj>
{
typedef int type;
};

et alors dans la classe ref, l'opérateur [] s'écrit:
template<typename X>
my_return_type<X>::type operator[](const X& p) const
{
return (*m_ptr)[p];
}

--
"I have nothing to declare except war on Austria." ~ Oscar Wilde, 1936

Avatar
Cyrille

X::return_type operator[](const X& p) const


erratum: c'est T::return_type

my_return_type<X>::type operator[](const X& p) const


erratum bis: my_return_type<T>::type

Ca m'apprendra à ne pas tester les morceaux de code que je poste ici :)

--
"I have nothing to declare except war on Austria." ~ Oscar Wilde, 1936

Avatar
kanze
Vincent Richard wrote:

Je dispose d'une classe de /smart pointer/ et je cherche à
implanter l'opérateur [] dessus, et déléguer l'appel à l'objet
encapsulé pour des raisons de confort d'utilisation :

template <typename T>
class ref // très simplifié...
{
public:

ref(T* ptr) : m_ptr(ptr) { }
~ref() { delete m_ptr; }

T& operator*() { return *m_ptr; }
T* operator->() { return m_ptr; }

template <typename X, typename Y>
Y operator[](const X& p) const


Qu'est-ce que ça doit signifier ? Si j'applique [] sur un
pointeur, la sémantique en est bien définie. Si j'ai un « smart
pointeur », si j'implémente operator[], il faut lui donner la
même sémantique, faute de semer la confusion chez l'utilisateur.
Ici, ça voudrait dire :

T& operator[]( size_t index ) const ;

(Et évidemment, comme pour les vrais pointeurs, ça n'a un sens
que si l'objet pointé est en fait un tableau.)

{
return (*m_ptr)[p];
}


Tu veux pouvoir utiliser l'objet pointé sans déréférencer le
pointeur, en somme. Pourquoi pas alors aussi ++, --, +-, -=,
getActivationState(), executeDailyJob()...

Pour commencer, si tu fais ça, ce n'est plus un smart pointeur,
mais un handle ou une façade. Et tu n'implémentes pas operator*
et operator-> -- un objet ne doit pas être à la fois un pointeur
vers l'objet, et l'objet même. Ensuite, si c'est effectivement
un modèle de conception assez utilisé, comme montre ma liste
ci-dessus, il faut bien une certaine connaissance de l'objet
derrière pour le faire ; l'interface de la façade est celle de
l'objet, et il n'y a pas de moyen (à ma connaissance) de le
faire génériquement.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Avatar
Vincent Richard
wrote:

Tu veux pouvoir utiliser l'objet pointé sans déréférencer le
pointeur, en somme.


C'est bien ce que je cherche à faire.

Pourquoi pas alors aussi ++, --, +-, -=, getActivationState(),
executeDailyJob()...


Parce que j'ai seulement besoin de l'opérateur [] ! :-) Je ne
voudrais pas casser du code existant qui repose sur cet opérateur.

J'ai une classe 'Part' qui permet de naviguer dans toute une
hiérarchie de sous-parties en utilisant l'opérateur [] en séquence,
par exemple :

myRootPart[0][2][5][3]

équivalent à :

myRootPart.getPartAt(0).getPartAt(2).getPartAt(5)...

Pour l'instant, les objets renvoyés sont des références (Part&)
mais j'aimerais passer ça en smart pointer (pour pouvoir conserver
des références fortes dessus), sans être obligé de faire :

(*(*(*(*myRootPart)[0])[2])[5])[3]

Je ne vois pas en quoi ça change la sémantique. C'est juste un
raccourci d'écriture en somme. Mais a priori, ce n'est pas faisable,
donc je laisse tomber...

Merci.

Vincent

Avatar
kanze
Vincent Richard wrote:
wrote:

Tu veux pouvoir utiliser l'objet pointé sans déréférencer le
pointeur, en somme.


C'est bien ce que je cherche à faire.


Ça me semble d'une mauvaise conception. Est-ce que ta classe est
un pointeur, ou une collection ? Il faut savoir. Sinon, ni les
utilisateurs, ni ceux qui ont à le maintenir n'y comprendront
rien.

Pourquoi pas alors aussi ++, --, +-, -=,
getActivationState(), > executeDailyJob()...


Parce que j'ai seulement besoin de l'opérateur [] ! :-)


Pour l'instant.

Je ne voudrais pas casser du code existant qui repose sur cet
opérateur.


Alors là, je ne comprends pas. Est-ce que le code existant
utilise des pointeurs à un seul objet, ou non ?

-- Si le code existant utilise des collections (avec
operator[]), il faut leur fournir une collection, non un
pointeur.

-- Si le code existant utilise des pointeurs à des tableaux,
ton opérateur[] doit renvoyer le même type que ton
operator*, et il n'y a pas de probleème.

-- Si le code existant utilise des pointeurs à des objets
simples, il n'utilise pas [] avec eux. Donc, pas de problème
non plus.

-- Si le code existant utilise déjà une classe qui est mal
conçue, et ton seule but y est de continuer à supporter
l'ancienne utilisation, afin de ne pas avoir à rétoucher le
code ancien, tu n'as prèsque sûrement pas besoin des
templates.

J'ai une classe 'Part' qui permet de naviguer dans toute une
hiérarchie de sous-parties en utilisant l'opérateur [] en
séquence, par exemple :

myRootPart[0][2][5][3]

équivalent à :

myRootPart.getPartAt(0).getPartAt(2).getPartAt(5)...


Donc, ce n'est pas un pointeur intelligent.

Pour l'instant, les objets renvoyés sont des références
(Part&) mais j'aimerais passer ça en smart pointer (pour
pouvoir conserver des références fortes dessus),


Je ne comprends pas. Qu'est-ce que tu entends par « références
fortes » ? Je ne vois pas l'intérêt à utiliser un pointeur
intelligent ici.

Je ne sais pas pourquoi les références ne te font pas l'affaire,
mais sinon, je verrais plutôt un envéloppe (dans l'idiome
lettre/envéloppe). Et de la dérivation et des classes de base,
plutôt que des templates.

sans être obligé de faire :

(*(*(*(*myRootPart)[0])[2])[5])[3]

Je ne vois pas en quoi ça change la sémantique. C'est juste un
raccourci d'écriture en somme. Mais a priori, ce n'est pas
faisable, donc je laisse tomber...


Il y a, en fait, peut-être une faiblesse du langage. Strictement
parlant, ce que tu veux, c'est une référence intelligente (et
non un pointeur intelligent). Ce qui est malheureusement
impossible, parce qu'on ne peut pas surcharger l'opérateur `.'.

Mais que même avec une référence intelligente, tu te trouves
avec le problème qu'il faut surcharger aussi les autres
opérateurs qui peuvent s'appliquer à une référence, c-à-d tout
opérateur que la classe à laquelle on fait référence a
surchargé. Et prèsque par définition, il n'y a pas de solution
générique, puisqu'il y a une infinité d'opérateurs
possibles. (Pense à () sans paramètres, () avec un paramètre, ()
avec deux paramètres...)

Ci dessus, en revanche, je crois que tu as donné la réponse : tu
n'as besoin que de l'opérateur []. C'est que tu ne recherches
pas une solution totalement générique, mais une solution à ton
problème exact. À partir de ce moment, je me démande si tu as
besoin de tant de templates. Un paramètre de template qui ne
serait jamais instancié que sur int ne me paraît peu
intéressant.

N'empêche que d'après le peu que tu as écrit, je verrais plutôt
une hièrarchie polymorphique, avec l'opérateur [] dans la classe
de base, qui renvoie une référence à la classe de base. À la
limite, l'opérateur[] pourrait être virtuel, avec un type de
retour covariant, mais je craindrais que ça apportera plus de
restrictions que de protection.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34