Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Pourquoi l'operateur -> const des smart ptr renvoie un non const ?

28 réponses
Avatar
Aurelien Regat-Barrel
Bonjour,
Je suis surpris et déçu de constater que plusieurs pointeurs
intelligents (auto_ptr, boost) ont leur opérateur d'indirection défini
ainsi:

T* operator->() const;

alors que je me serais attendu à:

T* operator->();
const T* operator->() const;

ce qui permet d'avoir une erreur de compilation dans l'exemple qui suit:

class A
{
public:
void f1() const {}
void f2() {}
};

const auto_ptr<A> p( new A ); // <- const
p->f1();
p->f2(); // erreur de compilation attendue mais...

Du coup il faut passer par un

auto_ptr<const A>

qui ne peut pas être implicitement converti depuis un

auto_ptr<A>

:/
(cela dit ça marche avec boost, dans une certaine mesure). Je supose
qu'il y a une bonne raison à celà, mais je ne la devine pas. La
connaissez-vous ?

Merci.

--
Aurélien Regat-Barrel

10 réponses

1 2 3
Avatar
Fabien LE LEZ
On Wed, 26 Apr 2006 18:09:05 +0200, Aurelien Regat-Barrel
:

Je suis surpris et déçu de constater que plusieurs pointeurs
intelligents (auto_ptr, boost) ont leur opérateur d'indirection défini
ainsi:

T* operator->() const;

Je supose qu'il y a une bonne raison à celà,


Une raison toute simple : un pointeur intelligent a une interface
aussi proche que possible de celle d'un pointeur "brut", pour pouvoir
facilement les remplacer.

Ainsi, un auto_ptr<T> fonctionne de la même façon qu'un T*.

De même, on a les correspondances :

auto_ptr<T const> <==> T const *
auto_ptr<T> const <==> T * const
auto_ptr<T const> const <==> T const * const

void f (int* const ptr)
{
int x= 0;
//ptr= &x; // Marche pas, car le pointeur est const
*ptr= x; // Fonctionne, car la variable pointée est non-const
}

Si tu remplaces int* par auto_ptr<int>, ça fonctionne de la même
façon.

Avatar
kanze
Aurelien Regat-Barrel wrote:

Je suis surpris et déçu de constater que plusieurs pointeurs
intelligents (auto_ptr, boost) ont leur opérateur
d'indirection défini ainsi:

T* operator->() const;

alors que je me serais attendu à:

T* operator->();
const T* operator->() const;


Pourquoi ? La const-ité d'un pointeur n'est pas la même chose
que la const-ité du pointé. À l'encontre d'une collection,
l'objet pointé ne fait pas partie du pointeur -- c'est un objet
complétement indépendant.

Pense au pointeurs bruts. Tu as bien :

int * pointeur_non_const_a_int_non_const ;
int * const pointeur_const_a_int_non_const ;
int const* pointeur_non_const_a_int_const ;
int const* const pointeur_const_a_int_const ;

Avec auto_ptr, ça donne :

auto_ptr< int > pointeur_non_const_a_int_non_const ;
auto_ptr< int > const pointeur_const_a_int_non_const ;
auto_ptr< int const > pointeur_non_const_a_int_const ;
auto_ptr< int const > const pointeur_const_a_int_const ;

Ou considère comment ça marche avec des typedef :

typedef int* Ptr ;
typedef int const* PtrConst ;
Ptr pointeur_non_const_a_int_non_const ;
Ptr const pointeur_const_a_int_non_const ;
PtrConst pointeur_non_const_a_int_const ;
PtrConst const pointeur_const_a_int_const ;

On peut alors remplacer les typedef avec :
typedef auto_ptr< int > Ptr ;
typedef auto_ptr< int const > PtrConst ;
sans problème, mais il faut toujours deux typedef, que ce soit
des pointeurs bruts ou des pointeurs intelligents.

(En passant, un peu de reflection sur les exemples ci-dessus
montrera clairement pourquoi il est préférable de toujours
mettre le const derrière, comme je fais.)

Note bien que dans le cas d'une collection, genre std::vector,
les éléments appartiennent à la collection, et font partie de
son état. On ne peut donc pas modifier un élément d'une
collection const. En revanche, les itérateurs fonctionnent comme
les pointeurs, ci-dessus : tu as un « iterator » et un
« const_iterator », et le const sur l'objet itérateur s'applique
à l'itérateur même, et non à l'élément auquel il réfère, c- à-d
:

vector< int >::iterator
iterateur_non_const_a_int_non_const ;
vector< int >::iterator const iterateur_const_a_int_non_const
;
vector< int >::const_iterator iterateur_non_const_a_int_const
;
vector< int >::const_iterator const iterateur_const_a_int_const ;

ce qui permet d'avoir une erreur de compilation dans l'exemple
qui suit:

class A
{
public:
void f1() const {}
void f2() {}
};

const auto_ptr<A> p( new A ); // <- const
p->f1();
p->f2(); // erreur de compilation attendue mais...


Mais tu ne veux pas d'erreur de compilation ici.

Du coup il faut passer par un

auto_ptr<const A>

qui ne peut pas être implicitement converti depuis un

auto_ptr<A>


Ça dépend où et comment. Les suivants marchent bien :
std::auto_ptr< A > pa( new A ) ;
std::auto_ptr< A const > pca( pa ) ;
pca = pa ;
Appeler une fonction qui prend un std::auto_ptr< A const > ou un
std::auto_ptr< A const > const&, en revanche, non.

:/
(cela dit ça marche avec boost, dans une certaine mesure).


Pas de problème avec shared_ptr, y compris avec les appels de
fonctions.

Je supose qu'il y a une bonne raison à celà, mais je ne la
devine pas. La connaissez-vous ?


La raison pourquoi la conversion implicite lors l'appel de
fonction avec auto_ptr ne marche pas, c'est lié au fait qu'on
(c-à-d ici, la délégation brittanique) a insisté que
std::vector< std::auto_ptr > provoque une erreur à la
compilation, plutôt que d'être simplement un comportement
indéfini. Ce qui fait que la sémantique d'auto_ptr est parfois
un peu bizarre. Dans la pratique, en revanche, je ne ai jamais
trouvé que c'est un problème -- si je passe un auto_ptr à une
fonction, l'objet qu'il désigne ne m'est plus accessible ; ça
m'est donc égal qu'il soit modifié ou non. Du coup, je crois que
je ne me suis jamais servi d'un auto_ptr à un const. Ce n'est
pas le cas de shared_ptr, évidemment. Mais shared_ptr n'a pas
besoin de tous ces bricolages pour provoquer une erreur dans
std::vector< std::shared_ptr >. Un vector des shared_ptr est
tout à fait légal, et a un comportement défini.

--
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
Aurelien Regat-Barrel
On Wed, 26 Apr 2006 18:09:05 +0200, Aurelien Regat-Barrel
:


Je suis surpris et déçu de constater que plusieurs pointeurs
intelligents (auto_ptr, boost) ont leur opérateur d'indirection défini
ainsi:

T* operator->() const;



Je supose qu'il y a une bonne raison à celà,



Une raison toute simple : un pointeur intelligent a une interface
aussi proche que possible de celle d'un pointeur "brut", pour pouvoir
facilement les remplacer.

Ainsi, un auto_ptr<T> fonctionne de la même façon qu'un T*.

De même, on a les correspondances :

auto_ptr<T const> <==> T const *
auto_ptr<T> const <==> T * const
auto_ptr<T const> const <==> T const * const

void f (int* const ptr)
{
int x= 0;
//ptr= &x; // Marche pas, car le pointeur est const
*ptr= x; // Fonctionne, car la variable pointée est non-const
}

Si tu remplaces int* par auto_ptr<int>, ça fonctionne de la même
façon.


Moui, je comprends. En fait c'est le même "piège" que typdef:

typedef A* APtr;

const APtr ptr1; // peut modifier *ptr1
const A* ptr2;

Cela dit, est-ce que tu serais choqué par un pointeur intelligent qui se
comporterait comme je le souhaite ?

--
Aurélien Regat-Barrel


Avatar
Aurelien Regat-Barrel

const auto_ptr<A> p( new A ); // <- const
p->f1();
p->f2(); // erreur de compilation attendue mais...



Mais tu ne veux pas d'erreur de compilation ici.


En répondant à Fabien je l'ai réalisé.

Du coup il faut passer par un
auto_ptr<const A>
qui ne peut pas être implicitement converti depuis un
auto_ptr<A>


Ça dépend où et comment. Les suivants marchent bien :
std::auto_ptr< A > pa( new A ) ;
std::auto_ptr< A const > pca( pa ) ;
pca = pa ;
Appeler une fonction qui prend un std::auto_ptr< A const > ou un
std::auto_ptr< A const > const&, en revanche, non.

:/
(cela dit ça marche avec boost, dans une certaine mesure).


Pas de problème avec shared_ptr, y compris avec les appels de
fonctions.


boost::shared_ptr faisait mon bonheur jusqu'à hier:

typedef boost::shared_ptr<A> APtr;
typedef std::vector<APtr> AVect;

typedef boost::shared_ptr<A const> AConstPtr;
typedef std::vector<AConstPtr> AConstVect;

en fait c'est std::vector le coupable:

void Test1( AConstPtr );
void Test2( AConstVect );

APtr ptr( new A );
AVect v;
v.push_back( ptr );

Test1( ptr ); // ok
Test2( v ); // pas content

du coup, j'hésite à utiliser const dans mes pointeurs intelligents, et
donc il perd son utilité au sein même de mes classes :-/

Je supose qu'il y a une bonne raison à celà, mais je ne la
devine pas. La connaissez-vous ?



La raison pourquoi la conversion implicite lors l'appel de
fonction avec auto_ptr ne marche pas, c'est lié au fait qu'on
(c-à-d ici, la délégation brittanique) a insisté que
std::vector< std::auto_ptr > provoque une erreur à la
compilation, plutôt que d'être simplement un comportement
indéfini. Ce qui fait que la sémantique d'auto_ptr est parfois
un peu bizarre. Dans la pratique, en revanche, je ne ai jamais
trouvé que c'est un problème -- si je passe un auto_ptr à une
fonction, l'objet qu'il désigne ne m'est plus accessible ; ça
m'est donc égal qu'il soit modifié ou non. Du coup, je crois que
je ne me suis jamais servi d'un auto_ptr à un const. Ce n'est
pas le cas de shared_ptr, évidemment. Mais shared_ptr n'a pas
besoin de tous ces bricolages pour provoquer une erreur dans
std::vector< std::shared_ptr >. Un vector des shared_ptr est
tout à fait légal, et a un comportement défini


Le contraire m'aurait mis dans l'embarras :-)

--
Aurélien Regat-Barrel


Avatar
kanze
Aurelien Regat-Barrel wrote:
On Wed, 26 Apr 2006 18:09:05 +0200, Aurelien Regat-Barrel
:

Je suis surpris et déçu de constater que plusieurs pointeurs
intelligents (auto_ptr, boost) ont leur opérateur
d'indirection défini ainsi:

T* operator->() const;

Je supose qu'il y a une bonne raison à celà,


Une raison toute simple : un pointeur intelligent a une
interface aussi proche que possible de celle d'un pointeur
"brut", pour pouvoir facilement les remplacer.

Ainsi, un auto_ptr<T> fonctionne de la même façon qu'un T*.

De même, on a les correspondances :

auto_ptr<T const> <==> T const *
auto_ptr<T> const <==> T * const
auto_ptr<T const> const <==> T const * const

void f (int* const ptr)
{
int x= 0;
//ptr= &x; // Marche pas, car le pointeur est const
*ptr= x; // Fonctionne, car la variable pointée est non-const
}

Si tu remplaces int* par auto_ptr<int>, ça fonctionne de la
même façon.


Moui, je comprends. En fait c'est le même "piège" que typdef:

typedef A* APtr;

const APtr ptr1; // peut modifier *ptr1
const A* ptr2;


Sauf que ce n'est pas réelement un piège, à mon avis. Si on
considère ce que signifie typedef -- ce n'est pas qu'un macro.

Mais comme j'ai dit dans ma réponse, si tu écrivais le const
derrière ce qu'il modifie, même en le considérant comme un
macro, tu n'auras pas de surprise. Ici, en tout cas.

Cela dit, est-ce que tu serais choqué par un pointeur
intelligent qui se comporterait comme je le souhaite ?


Assez, oui. Un pointeur ne se modifie pas si on modifie le
pointé ; c'est donc normal que le const du pointeur n'affecte
pas l'accéssibilité de l'objet.

--
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
kanze
Aurelien Regat-Barrel wrote:

const auto_ptr<A> p( new A ); // <- const
p->f1();
p->f2(); // erreur de compilation attendue mais...


Mais tu ne veux pas d'erreur de compilation ici.


En répondant à Fabien je l'ai réalisé.

Du coup il faut passer par un
auto_ptr<const A>
qui ne peut pas être implicitement converti depuis un
auto_ptr<A>


Ça dépend où et comment. Les suivants marchent bien :
std::auto_ptr< A > pa( new A ) ;
std::auto_ptr< A const > pca( pa ) ;
pca = pa ;
Appeler une fonction qui prend un std::auto_ptr< A const >
ou un std::auto_ptr< A const > const&, en revanche, non.

:/
(cela dit ça marche avec boost, dans une certaine mesure).


Pas de problème avec shared_ptr, y compris avec les appels
de fonctions.


boost::shared_ptr faisait mon bonheur jusqu'à hier:

typedef boost::shared_ptr<A> APtr;
typedef std::vector<APtr> AVect;

typedef boost::shared_ptr<A const> AConstPtr;
typedef std::vector<AConstPtr> AConstVect;

en fait c'est std::vector le coupable:

void Test1( AConstPtr );
void Test2( AConstVect );

APtr ptr( new A );
AVect v;
v.push_back( ptr );

Test1( ptr ); // ok
Test2( v ); // pas content


Et est-ce que ça marcherait avec :
typedef A* APtr ;
typedef A const* AConstPtr ;
?

Il y a des limites de ce qu'on peut faire avec des conversions
de type implicites. Est-ce que tu veux qu'on puisse convertir
tous les vecteurs, du moment que le type contenu dans un se
laisse convertir en type contenu dans l'autre. Que je puisse
affeecter un vector<double> à un vector<char>, c-à-d :

typedef std::vector< char > CVect ;
typedef std::vector< double > DVect ;

void Test( CVect ) ;

DVect dv ;
Test( dv ) ;

Note aussi que c'est assez rare de passer des vecteurs par
valeur. Mais si on passe par référence (const), attention au
dégat :

typedef std::vector< char > CVect ;
void Test( CVect const& ) ;

DVect dv ;
Test( dv ) ;

Du coup, j'ai une copie profonde que je n'ai peut-être pas
voulue. Ça coûte cher, copier tout un vecteur.

Note bien que si tu veux la copie, tu peux toujours écrire :

Test2( AConstVect( v.begin(), v.end() ) ) ;

C'est la façon à appliquer la conversion explicite sur chaque
élément du vecteur.

du coup, j'hésite à utiliser const dans mes pointeurs
intelligents, et donc il perd son utilité au sein même de mes
classes :-/


Je ne sais pas. Dans l'ensemble, je crois que boost::shared_ptr
se comporte assez comme un pointeur brut à cet égard. Et j'avoue
que ces restrictions ne m'ont jamais causé de problèmes. Les
copies pûres et simples des vecteurs sont plutôt rares dans mon
code ; ce qui arrive souvent, en revanche, ce sont des
sous-ensembles -- une copie qui ne copie que certains éléments.
Et dans ce cas-là, la solution la plus simple, c'est un
itérateur filtrant de Boost, et le constructeur à deux
itérateurs du vecteur. Mais alors, si le sous-ensemble doit être
un vector< AConstPtr >, plutôt qu'un vector< APtr >, pas de
problème -- parce que je me sers du constructeur à deux
itérateurs, et que donc le compilateur cherche à faire la
conversion élément par élément, plutôt que sur le vecteur
complet.

--
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
Fabien LE LEZ
On Thu, 27 Apr 2006 11:06:30 +0200, Aurelien Regat-Barrel
:

Cela dit, est-ce que tu serais choqué par un pointeur intelligent qui se
comporterait comme je le souhaite ?


Tu peux faire une classe qui se comporte comme tu le souhaites.
L'appeler "pointeur" me choquerait effectivement.

Avatar
Aurelien Regat-Barrel

Moui, je comprends. En fait c'est le même "piège" que typdef:



typedef A* APtr;

const APtr ptr1; // peut modifier *ptr1
const A* ptr2;



Sauf que ce n'est pas réelement un piège, à mon avis. Si on
considère ce que signifie typedef -- ce n'est pas qu'un macro.

Mais comme j'ai dit dans ma réponse, si tu écrivais le const
derrière ce qu'il modifie, même en le considérant comme un
macro, tu n'auras pas de surprise. Ici, en tout cas.


Je le sais, mais c'est une question d'habitudes : const char *,
const_iterator, ...

Cela dit, est-ce que tu serais choqué par un pointeur
intelligent qui se comporterait comme je le souhaite ?



Assez, oui. Un pointeur ne se modifie pas si on modifie le
pointé ; c'est donc normal que le const du pointeur n'affecte
pas l'accéssibilité de l'objet.


De toutes façons ça résoud pas le problème de mon vector<const>.

--
Aurélien Regat-Barrel


Avatar
Aurelien Regat-Barrel

Cela dit, est-ce que tu serais choqué par un pointeur intelligent qui se
comporterait comme je le souhaite ?



Tu peux faire une classe qui se comporte comme tu le souhaites.
L'appeler "pointeur" me choquerait effectivement.


Je vais continuer avec shared_ptr alors.

--
Aurélien Regat-Barrel


Avatar
Fabien LE LEZ
On Thu, 27 Apr 2006 22:03:36 +0200, Fabien LE LEZ
:

Tu peux faire une classe qui se comporte comme tu le souhaites.
L'appeler "pointeur" me choquerait effectivement.


En fait, je m'aperçois que je fais assez souvent des classes
(non templates) de ce style, et que je les appelle généralement
"handle", "gestionnaire", ou quelque chose d'approchant.
Mais ces classes n'ont pas d'opérateur -> ou * : elles ont grosso modo
les mêmes fonctions que la classe "de base".

Par exemple :

class Machin
{
public:
void truc();
Machin (int n);
};

class HandleMachin
{
public:
HandleMachin() : ptr (0) {}
void CreerMachin (int n)
{ Machin* m= new Machin (n); std::swap(m,ptr); delete m; }
bool EstOk() const { return ptr != 0; }
void truc() { assert(EstOk()); ptr->truc(); }
~HandleMachin() { delete ptr; }
private:
Machin *ptr;
};

C'est un peu l'idiome "pimpl", sauf que le point de vue n'est pas le
même : pimpl sert à cacher les rouages internes d'une classe ; cette
technique-ci sert plutôt à faciliter l'usage d'une classe déjà faite.
Un peu comme un pointeur intelligent, en somme.

1 2 3