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

Exceptionnal C++ de Sutter => 2 questions

8 réponses
Avatar
Michael
Bonsoir =E0 tous,

voici mes 2 questions:

dans la partie: rendre son code const-correct, il parle de mettre
const toutes les fonctions qui ne modifient pas l'=E9tat visible de
l'objet. Qu'entend-il par l=E0?

Je n'ai pas le livre sous la main, mais je me souviens de ce genre
d'exemple:

class foo
{
private:
int surface_;
public:
int GetSurface() const { return surface_; }
void toto()
{
if (mustCalcSurface)
surface_ =3D //blabla;
}
};

Et il recommandait de rendre toto() const et du coup surface_ mutable,
dans la mesure o=F9 toto() ne modifie pas l'=E9tat visible de l'objet.

Deuxi=E8me question:

il recommande d'utiliser l'idiome Create and Swap dans l'op=E9rateur
d'affectation afin de le rendre "strongly exception safe", et
d'utiliser le constructeur par copie ainsi qu'une fonction Swap()
d=E9clar=E9e nothrow()

Mais dans le cas suivant:

class Base
{
private:
int i_;
protected:
int GetI() { return i_; }
void Calc() =3D 0;
}

class Derived : public Base
{
public:
Derived(const Derived & other) : i_(?) {}
Derived & operator=3D(const Derived & other)
{
Derived temp(other);
Swap(temp);
return *this;
}

void Swa^p(Derived & other) throw()
{
std::swap(i_,??);
}

Merci d'avance

8 réponses

Avatar
Fabien LE LEZ
On Mon, 24 Sep 2007 16:51:30 -0700, Michael :

Mais dans le cas suivant:


Dans ce cas, les constructeur et opérateur de copie par défaut
conviennent parfaitement.

L'idiome "swap" ne sert que quand les constructeur et opérateur de
copie par défaut ne conviennent pas -- typiquement, quand un membre
est un pointeur.

Avatar
Michael DOUBEZ
Bonsoir à tous,

voici mes 2 questions:

dans la partie: rendre son code const-correct, il parle de mettre
const toutes les fonctions qui ne modifient pas l'état visible de
l'objet. Qu'entend-il par là?

Je n'ai pas le livre sous la main, mais je me souviens de ce genre
d'exemple:

class foo
{
private:
int surface_;
public:
int GetSurface() const { return surface_; }
void toto()
{
if (mustCalcSurface)
surface_ = //blabla;
}
};

Et il recommandait de rendre toto() const et du coup surface_ mutable,
dans la mesure où toto() ne modifie pas l'état visible de l'objet.


Pour comprendre mutable, un exemple classique est la lecture d'un état
protégé par un mutex:
class foo
{
private:
int surface_;
mutable mutex surface_mutex_;
public:
int GetSurface() const
{
mutex::scoped_lock lock(surface_mutex_);
return surface_;
}
};
Dans cet exemple, tu as besoin de locker ton mutex mais la fonction et
const vis à vis de l'état interne (i.e. la valeur de la surface). Sans
le keyword mutable, tu ne pourrais pas déclarer GetSurface() const.

Dans l'exemple que tu donnes, c'est un peu plus complexe: le calcul de
la surface est déféré à la première lecture de celle-ci (en lazy). La
lecture de la surface ne modifie pas l'état de l'objet du point de vue
de l'utilisateur mais du point de vue de l'implémentation, l'état de
l'objet change (la surface n'était pas calculée et devient disponible
directement). Tu as donc besoin de déclarer ton membre mutable.

Par exemple avec un carré:

class carre
{
private:
mutable int surface_;
int cote_; // longueur coté du carré
mutable bool surf_ok_; // si faux - surface_ à calculer
public:
carre(int c=0):cote_(c),surf_ok_(false){}

void set_cote(int c){surf_ok_úlse;cote_=c;}

int getSurface() const
{
if(!surf_ok_)
{
surface_=cote_*cote_;
surf_ok_=true;
}
return surface_;
}
};


Deuxième question:

il recommande d'utiliser l'idiome Create and Swap dans l'opérateur
d'affectation afin de le rendre "strongly exception safe", et
d'utiliser le constructeur par copie ainsi qu'une fonction Swap()
déclarée nothrow()


Répondu par Fabien LE LEZ.
Ca s'applique le cas ou une opération peut lancer une exception ce qui
n'est pas le cas dans ton exemple.

void Swa^p(Derived & other) throw()
{
std::swap(i_,??);
}


Le but est justement que l'opération de swap soit nothrow donc pas
d'indication throw().


Michael

Avatar
James Kanze
On Sep 25, 1:51 am, Michael wrote:
dans la partie: rendre son code const-correct, il parle de
mettre const toutes les fonctions qui ne modifient pas l'état
visible de l'objet. Qu'entend-il par là?


L'état visible, c'est l'état que peut voir un client,
directement ou indirectement. Un état qui influence le
comportement de l'objet est visible, même si le client ne peut
pas y accéder directement.

Je n'ai pas le livre sous la main, mais je me souviens de ce genre
d'exemple:

class foo
{
private:
int surface_;
public:
int GetSurface() const { return surface_; }
void toto()
{
if (mustCalcSurface)
surface_ = //blabla;
}
};

Et il recommandait de rendre toto() const et du coup surface_
mutable, dans la mesure où toto() ne modifie pas l'état
visible de l'objet.


Je ne crois pas que tu te souviens correctement. En fait, si
c'est quelque chose comme ça, je crois il s'agit plutôt d'une
valeur calculée de façon paresseuse. Quelque chose comme :

class Toto
{
private:
bool mutable surfaceIsValid ;
int mutable surface ;
// ...

public:
int getSurface() const
{
if ( ! surfaceIsValid ) {
surface = // ...
surfaceIsValid = true ;
}
return surface ;
}
// ...
} ;

Dans ce cas-là, on suppose qu'en fait, la supérficie dépend
d'autres variables de la classe, qu'elle soit assez chère à
calculer, qu'on n'en a pas toujours besoin, mais qu'il arrive
qu'on en a besoin plusieurs fois de suite. Alors, chaque fois
qu'on modifie une variable qui agit sur la valeur de la
supérficie, on met surfaceIsValid à faux, plutôt que de calculer
la supérficie de nouveau, et chaque fois qu'on a besoin de la
supérficie, on appelle getSurface(). Du point de vue du client, la
supérficie fait plus ou moins partie de l'état visible, mais
elle n'est pas modifiée par la fonction getSurface(), mais
plutôt par les fonctions qui ont changé les autres variables (et
mis surfaceIsValid à faux). La fonction getSurface() ne modifie
pas l'état visible de l'objet, mais simplement comment cet état
est représenté dans l'objet : avant l'appel, l'état est
implicit, une fonction des autres variables, et après, explicit,
dans la variable surface.

Deuxième question:

il recommande d'utiliser l'idiome Create and Swap dans
l'opérateur d'affectation afin de le rendre "strongly
exception safe",


Je me démande s'il dirait encore la même chose aujourd'hui.
C'est une façon de rendre "strongly exception safe", mais ce
n'est pas la seule façon, et on constate depuis qu'il a écrit le
livre qu'on n'a pas vraiment besoin de "strongly exception safe"
aussi souvent que ça. (Mais c'est toujours un idiome à
connaître. C'est parfois la façon la plus simple et la plus sûr
de rendre "strongly exception safe", si on en a besoin.)

et d'utiliser le constructeur par copie ainsi qu'une fonction
Swap() déclarée nothrow()

Mais dans le cas suivant:

class Base
{
private:
int i_;
protected:
int GetI() { return i_; }
void Calc() = 0;
}

class Derived : public Base
{
public:
Derived(const Derived & other) : i_(?) {}
Derived & operator=(const Derived & other)
{
Derived temp(other);
Swap(temp);
return *this;
}

void Swap(Derived & other) throw()
{
std::swap(i_,??);
}


L'idiome de swap ne s'applique que si tous les membres et les
bases possèdent une fonction de swap nothrow. Dans ce cas-ci,
par exemple, on ne pourrait s'en servir que si Base avait
elle-meme un swap(), c-à-d :

class Base
{
private:
int i ;

public:
// ...
void swap( Base& other ) throw()
{
std::swap( i, other.i ) ;
}
Base& operator=( Base const& other )
{
Base tmp( other ) ;
swap( tmp ) ;
return *this ;
}
} ;

class Derived : public Base
{
public:
void swap( Derived& other ) throw()
{
Base::swap( other ) ;
// ...
}

Derived& operator=( Derived const& other )
{
Derived tmp( other ) ;
swap( tmp ) ;
return *this ;
}
} ;

(Évidemment, il ne s'applique pas à des classes aussi simple. Il
ne se justifie en fait que s'il y a des affectations des
sous-objets qui pourrait autrement échouer, à cause d'une
allocation dynamique, par exemple. Et aussi, c'est très, très
rare que des classes dans une hièrarchie polymorphique
supportent l'affectation, et quand elles le font, on se sert de
l'idiome de lettre/envéloppe, qui a d'autres contraintes, et ce
n'est que la classe de base qui supporte l'affectation, pas les
classes dérivées. Mais exactement les mêmes considérations
s'appliquent aux classes avec des membres plus ou moins
compliqués.)

--
James Kanze (GABI Software) email:
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
DELVA Michael
Mais dans le cas suivant:


Dans ce cas, les constructeur et opérateur de copie par défaut
conviennent parfaitement.

L'idiome "swap" ne sert que quand les constructeur et opérateur de
copie par défaut ne conviennent pas -- typiquement, quand un membre
est un pointeur.


Certes, mon exemple est un peu simpliste, mais je voulais savoir comment
réaliser ceci alors que des membres qui doivent être modifiés sont déclarés
private dans une classe de base.

Une piste m'a été donné par James.

Merci pour ta réponse


Avatar
DELVA Michael
Merci pour l'exemple sur mutable, c'est un peu plus clair maintenant

Ca s'applique le cas ou une opération peut lancer une exception ce qui
n'est pas le cas dans ton exemple.

void Swa^p(Derived & other) throw()
{
std::swap(i_,??);
}


Le but est justement que l'opération de swap soit nothrow donc pas
d'indication throw().


Pourtant Sutter mets bien throw() à la fin de la fonction Swap()


Avatar
DELVA Michael
il recommande d'utiliser l'idiome Create and Swap dans
l'opérateur d'affectation afin de le rendre "strongly
exception safe",


Je me démande s'il dirait encore la même chose aujourd'hui.
C'est une façon de rendre "strongly exception safe", mais ce
n'est pas la seule façon, et on constate depuis qu'il a écrit le
livre qu'on n'a pas vraiment besoin de "strongly exception safe"
aussi souvent que ça. (Mais c'est toujours un idiome à
connaître. C'est parfois la façon la plus simple et la plus sûr
de rendre "strongly exception safe", si on en a besoin.)


Quelles autres méthodes existe-il alors?

Qu'est-ce que tu entends par "pas aussi souvent que ça"? J'imagine que
les allocations / Déallocations se doivent de l'être, non?

L'idiome de swap ne s'applique que si tous les membres et les
bases possèdent une fonction de swap nothrow. Dans ce cas-ci,
par exemple, on ne pourrait s'en servir que si Base avait
elle-meme un swap(), c-à-d :

class Base
{
private:
int i ;

public:
// ...
void swap( Base& other ) throw()
{
std::swap( i, other.i ) ;
}
Base& operator=( Base const& other )
{
Base tmp( other ) ;
swap( tmp ) ;
return *this ;
}
} ;

class Derived : public Base
{
public:
void swap( Derived& other ) throw()
{
Base::swap( other ) ;
// ...
}

Derived& operator=( Derived const& other )
{
Derived tmp( other ) ;
swap( tmp ) ;
return *this ;
}
} ;

(Évidemment, il ne s'applique pas à des classes aussi simple. Il
ne se justifie en fait que s'il y a des affectations des
sous-objets qui pourrait autrement échouer, à cause d'une
allocation dynamique, par exemple. Et aussi, c'est très, très
rare que des classes dans une hièrarchie polymorphique
supportent l'affectation, et quand elles le font, on se sert de
l'idiome de lettre/envéloppe, qui a d'autres contraintes, et ce
n'est que la classe de base qui supporte l'affectation, pas les
classes dérivées. Mais exactement les mêmes considérations
s'appliquent aux classes avec des membres plus ou moins
compliqués.)


Qu'est-ce que cet idiome de lettre / enveloppe?

Merci pour tes réponses


Avatar
Sylvain
Michael DOUBEZ wrote on 25/09/2007 09:20:

void Swa^p(Derived & other) throw()


Le but est justement que l'opération de swap soit nothrow donc pas
d'indication throw().


la syntaxe de "throw()" est de placer en argument la liste des
exceptions pouvant être levées, donc un "nothrow" se code bien:
"throw(/* aucune */)"

Sylvain.


Avatar
James Kanze
On Sep 25, 2:53 pm, DELVA Michael wrote:
il recommande d'utiliser l'idiome Create and Swap dans
l'opérateur d'affectation afin de le rendre "strongly
exception safe",


Je me démande s'il dirait encore la même chose aujourd'hui.
C'est une façon de rendre "strongly exception safe", mais ce
n'est pas la seule façon, et on constate depuis qu'il a écrit le
livre qu'on n'a pas vraiment besoin de "strongly exception safe"
aussi souvent que ça. (Mais c'est toujours un idiome à
connaître. C'est parfois la façon la plus simple et la plus sûr
de rendre "strongly exception safe", si on en a besoin.)


Quelles autres méthodes existe-il alors?


En gros, tout ce qu'il faut, c'est de s'assurer que tous ce qui
peut lever une exception s'effectuent avant de changer l'état.
Donc, par exemple, dans l'idiome du pare-feu de compilation,
avec une copie profonde, on pourrait écrire :

MaClasse&
MaClasse::operator=( MaClasse const& other )
{
Impl* tmp = new Impl( *other.myImpl ) ;
delete myImpl ;
myImpl = tmp ;
return *this ;
}

Ou on utilise un pointeur intelligent, du genre
boost::scoped_ptr, et on écrit simplement :

MaClasse&
MaClasse::operator=( MaClasse const& other )
{
myImpl = new Impl( *other.myImpl ) ;
return *this ;
}

Le point de séquencement lors de l'appel de l'operator= du
pointeur s'assure que le new s'est complètement bien passé avant
le delete (qui se trouve dand l'operator= du pointeur).

Qu'est-ce que tu entends par "pas aussi souvent que ça"?
J'imagine que les allocations / Déallocations se doivent de
l'être, non?


Être quoi ? L'allocation même doit offrir la garantie forte,
évidemment. Mais ce n'est pas moi qui l'écrit. Considère une
classe qui comporte une série de membres : des types de base,
mais aussi des std::vector, des std::string, des pointeurs
intelligents. Tu peux écrire l'operator= d'une façon tout
bête :

MaClasse&
MaClasse::operator=( MaClasse const& other )
{
mySmartPtr = new T1( *other.mySmartPtr ) ;
myVector = other.myVector ;
myString = other.myString ;
// ...
return *this ;
}

S'il y a une exception quelque part au milieu, la garantie forte
n'est pas maintenue, mais comme j'ai dit, souvent, ce n'est pas
grave. Les objets qu'on copie ont une sémantique de valeur.
Alors, ou bien, ils sont temporaires, et qui seront detruits
sans servir dans le cas d'une exception, ou bien ils font partie
d'un objet entité, et la gestion de la transaction s'assurera
que l'objet entité entier retrouve son état initial ; pour les
objets entité, la plus part du temps, la garantie forte des
objets valeur qu'il contient ne suffit pas pour assurer
l'integrité transactionnelle. (Ce n'est peut-être pas toujours
le cas, mais la plupart du temps, une exception provoque
l'interruption de la transaction, avec un roll-back.)

[...]
Qu'est-ce que cet idiome de lettre / enveloppe?


C'est l'idiome plus ou moins standard de donner un comportement
polymorphique à un objet de valeur. La classe de base comporte
un pointeur à l'instance qui sert réelement, et toutes ces
fonctions virtuelles renvoient à l'instance à laquelle il
pointe. Sans entrer trop dans les détails (qui sont bien
expliqués dans le Coplien), la classe de base pourrait
ressembler :

class Base
{
public:
Base( params1 ) : myImpl( new Derived1( params1 ) ) {}
Base( params2 ) : myImpl( new Derived1( params2 ) ) {}

Base( Base const& other )
: myImpl( other.myImpl->clone() )
{
}

Base& operator=( Base const& other )
{
assert( myImpl != NULL ) ;
Base* tmp = other.myImpl->clone() ;
delete myImpl ;
myImpl = tmp ;
return *this ;
}

virtual ~Base() { delete myImpl }

// ...
virtual void f()
{
myImpl->f() ;
}
// ...

protected:
Base() : myImpl( NULL ) {}

private:
virtual Base* clone() const {
assert( 0 ) ;
}

private:
Base* myImpl ;
} ;

L'idée, c'est que le client ne manipule que des Base (sauf,
éventuellement, quand il crée une instance), qui ont une
sémantique de valeur. C'est l'indirection dans la base qui rend
le comportement polymorphique.

--
James Kanze (GABI Software) email:
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