OVH Cloud OVH Cloud

[g++/Valgrind] Problème étrange

11 réponses
Avatar
Vincent Richard
Bonsoir,

Voici un extrait de code très simple (et totalement inutile) :

================================================
struct A
{
virtual ~A() { }

A& operator=(const A& a)
{
f();
return (*this);
}

virtual void f() = 0;
};

struct B : public A
{
B() { p = new int; }
~B() { delete (p); }

void f() { }

int* p;
};

int main()
{
B b1;
B b2;

b1 = b2;
}
================================================

Je compile (g++ 3.4.2), j'exécute et j'obtiens un comportement étrange,
notifié par Valgrind ("Invalid free() / delete...") :

vincent@sherlock:~/tmp$ valgrind ./op
==10368== Memcheck, a memory error detector for x86-linux.
==10368== Copyright (C) 2002-2004, and GNU GPL'd, by Julian Seward et al.
==10368== Using valgrind-2.2.0, a program supervision framework for
x86-linux.
==10368== Copyright (C) 2000-2004, and GNU GPL'd, by Julian Seward et al.
==10368== For more details, rerun with: -v
==10368==
==10368== Invalid free() / delete / delete[]
==10368== at 0x1B905616: operator delete(void*) (vg_replace_malloc.c:156)
==10368== by 0x8048698: B::~B() (op.cpp:18)
==10368== by 0x804866D: main (op.cpp:30)
==10368== Address 0x1BB5C060 is 0 bytes inside a block of size 4 free'd
==10368== at 0x1B905616: operator delete(void*) (vg_replace_malloc.c:156)
==10368== by 0x8048698: B::~B() (op.cpp:18)
==10368== by 0x8048639: main (op.cpp:30)
==10368==
==10368== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 17 from 1)
==10368== malloc/free: in use at exit: 4 bytes in 1 blocks.
==10368== malloc/free: 2 allocs, 2 frees, 8 bytes allocated.
==10368== For a detailed leak analysis, rerun with: --leak-check=yes
==10368== For counts of detected errors, rerun with: -v

Bizarrement, lorsque j'enlève la ligne "b1 = b2", tout se passe normalement
(pas de fuite mémoire, pas de problème de delete(), etc.). Dans les deux
cas, l'exécution ne provoque aucune erreur (mis à part le message de
Valgrind dans le premier cas).

J'ai un peu de mal à comprendre ce qu'il se passe, ce code étant pourtant
parfaitement valide, peut-être que quelqu'un ici pourra m'éclairer...

Je soupçonne un bug de Valgrind mais je voudrais être sûr.

Merci d'avance pour vos réponses.

Vincent

--
vmime, une bibliothèque C++ sous licence GPL pour parser et générer
des messages au format MIME : http://www.sourceforge.net/projects/vmime/

10 réponses

1 2
Avatar
Michel Michaud
Dans le message 4175898c$0$30856$,
struct B : public A
{
B() { p = new int; }
~B() { delete (p); }

void f() { }

int* p;
};

int main()
{
B b1;
B b2;

b1 = b2;
}
=============================================== >
Je compile (g++ 3.4.2), j'exécute et j'obtiens un comportement
étrange, notifié par Valgrind ("Invalid free() / delete...") :


Tu as deux B partageant un lien vers la zone allouée dynamiquement.
Il y a donc deux destructeurs qui essaieront de la libérer, d'où
l'erreur. Il faut apprendre à faire les constructeur de copie et
operator= nécessaires.

--
Michel Michaud
http://www.gdzid.com
FAQ de fr.comp.lang.c++ :
http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ/

Avatar
Vincent Richard
Tu as deux B partageant un lien vers la zone allouée dynamiquement.
Il y a donc deux destructeurs qui essaieront de la libérer, d'où
l'erreur. Il faut apprendre à faire les constructeur de copie et
operator= nécessaires.


Aïe, c'était tout bête, et crois que je viens de comprendre mon erreur.

En fait, dans le code d'origine, f() est en fait copyFrom() qui se
charge de copier l'objet. Ce copyFrom() est donc appelée par
operator=() de la classe de base :

struct A
{
virtual ~A() { }

A& operator=(const A& a)
{
copyFrom(a);
return (*this);
}

virtual void copyFrom(const A& a) = 0;
};

struct B : public A
{
void B::copyFrom(const A& a)
{
const B& b = dynamic_cast <const B&>(a);

*p = *(b.p)
}
};

Il ne m'a pas semblé nécessaire de le mettre dans mon code de test
car dans tous les cas, l'erreur se produisait (du fait du B::operator implicite indiqué plus loin).

Il semblerait que quand on fait :

b1 = b2;

ce n'est pas A::operator=() qui est appelée (pas en premier), mais
B::operator=(), qui est créée implicitement par le compilateur (et qui ne
fait qu'une "bête" copie de pointeur).

En effet, ça fonctionne correctement en ajoutant :

B& B::operator=(const B& b)
{
A::operator=(b);
return (*this);
}

Il est donc obligatoire d'implanter operator=() dans toutes les
classes (et pas seulement sur A) ? (même si c'est juste un appel
à A::operator=)

Est-ce exact ?

Merci.

Vincent

--
vmime, une bibliothèque C++ sous licence GPL pour parser et générer
des messages au format MIME : http://www.sourceforge.net/projects/vmime/

Avatar
kanze
Vincent Richard wrote
in message news:<4175898c$0$30856$...

Voici un extrait de code très simple (et totalement inutile) :

=============================================== > struct A
{
virtual ~A() { }

A& operator=(const A& a)
{
f();
return (*this);
}

virtual void f() = 0;
};

struct B : public A
{
B() { p = new int; }
~B() { delete (p); }

void f() { }

int* p;
};

int main()
{
B b1;
B b2;

b1 = b2;
}
===============================================
Je compile (g++ 3.4.2), j'exécute et j'obtiens un comportement
étrange, notifié par Valgrind ("Invalid free() / delete...") :

:~/tmp$ valgrind ./op
=368== Memcheck, a memory error detector for x86-linux.
=368== Copyright (C) 2002-2004, and GNU GPL'd, by Julian Seward et al.
=368== Using valgrind-2.2.0, a program supervision framework for
x86-linux.
=368== Copyright (C) 2000-2004, and GNU GPL'd, by Julian Seward et al.
=368== For more details, rerun with: -v
=368==
=368== Invalid free() / delete / delete[]
=368== at 0x1B905616: operator delete(void*) (vg_replace_malloc.c:156)
=368== by 0x8048698: B::~B() (op.cpp:18)
=368== by 0x804866D: main (op.cpp:30)
=368== Address 0x1BB5C060 is 0 bytes inside a block of size 4 free'd
=368== at 0x1B905616: operator delete(void*) (vg_replace_malloc.c:156)
=368== by 0x8048698: B::~B() (op.cpp:18)
=368== by 0x8048639: main (op.cpp:30)
=368==
=368== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 17 from 1)
=368== malloc/free: in use at exit: 4 bytes in 1 blocks.
=368== malloc/free: 2 allocs, 2 frees, 8 bytes allocated.
=368== For a detailed leak analysis, rerun with: --leak-check=yes
=368== For counts of detected errors, rerun with: -v

Bizarrement, lorsque j'enlève la ligne "b1 = b2", tout se passe
normalement (pas de fuite mémoire, pas de problème de delete(), etc.).
Dans les deux cas, l'exécution ne provoque aucune erreur (mis à part
le message de Valgrind dans le premier cas).


Normal. Tu as negligé la règle des quatre : s'il te faut un des quatre
suivants : constructeur de copie, constructeur de défaut, opérateur
d'affectation, destructeur ; il t'en faut tous les quatre. (Il y a une
exception quand la raison pour le destructeur est de le rendre virtuel,
ou qu'on a fourni un des quatre seulement afin qu'il ne soit pas
public.)

Lire Scott Meyers.

J'ai un peu de mal à comprendre ce qu'il se passe, ce code étant
pourtant parfaitement valide, peut-être que quelqu'un ici pourra
m'éclairer...

Je soupçonne un bug de Valgrind mais je voudrais être sûr.


C'est un peu effronté de ta part, non ? Manifestement, tu n'as jamais
encore écrit un programme en C++, et cependant, du coup « ce code étant
pourtant parfaitement valide » (il ne l'est pas, et la règle qu'il viole
est un des plus fondamentale) et un boggue dans Valgrind.

--
James Kanze GABI Software http://www.gabi-soft.fr
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
Normal. Tu as negligé la règle des quatre : s'il te faut un des quatre
suivants : constructeur de copie, constructeur de défaut, opérateur
d'affectation, destructeur ; il t'en faut tous les quatre. (Il y a une
exception quand la raison pour le destructeur est de le rendre virtuel,
ou qu'on a fourni un des quatre seulement afin qu'il ne soit pas
public.)

Lire Scott Meyers.


Merci du conseil.

Mais pourquoi donc ce n'est pas l'operator=() de la classe de base qui est
appelé quand il n'est pas défini dans une classe ? Y'a-t-il une raison à
cela ?

En clair, je cherchais à n'écrire operator=() qu'une seule fois (un peu
par feignantise, je l'avoue :-)), et à implanter, par contre, un copyFrom()
dans toutes les classes.

Je soupçonne un bug de Valgrind mais je voudrais être sûr.


C'est un peu effronté de ta part, non ?


En effet, je me suis un peu emporté et j'en suis désolé (d'autant plus que
Valgrind m'a déjà plusieurs fois permis de me trouver de grosses erreurs).
Peut-être faudrait-il que je travaille un peu moins tard le soir...

Manifestement, tu n'as jamais encore écrit un programme en C++, et
cependant, du coup « ce code étant pourtant parfaitement valide » (il ne
l'est pas, et la règle qu'il viole est un des plus fondamentale) et un
boggue dans Valgrind.


Je ne prétend nullement être un expert, et d'ailleurs c'est comme tout,
il faut bien apprendre un jour (même si ça fait un moment que je m'y
suis mis).

Pour le "parfaitement" valide, je m'appuyais en fait sur le fait que mon
compilateur (g++) ne sortait aucun warning (même avec les flags -W -Wall
-ansi -pedantic). Maintenant, tout le monde n'a pas la norme C++ comme
livre de chevet...

Ce n'était tout de même pas la peine de prendre un ton si méprisable.

Bonne journée.

Vincent

--
vmime, une bibliothèque C++ sous licence GPL pour parser et générer
des messages au format MIME : http://www.sourceforge.net/projects/vmime/


Avatar
James Kanze
Vincent Richard writes:

|> Mais pourquoi donc ce n'est pas l'operator=() de la classe de base
|> qui est appelé quand il n'est pas défini dans une classe ?

Il est appelé. Quand tu ne définis pas d'opérateur d'affectation, le
compilateur en génère un, qui appelle les opérateurs d'affectation pour
toutes les classes de base et pour toutes les membres.

|> Y'a-t-il une raison à cela ?

|> En clair, je cherchais à n'écrire operator=() qu'une seule fois (un
|> peu par feignantise, je l'avoue :-)), et à implanter, par contre, un
|> copyFrom() dans toutes les classes.

Et qu'est ce que ça change ?

(En fait, c'est assez rare qu'une classe polymorphique supporte
l'affectation. L'héritage et l'affectation ne vont pas bien ensemble.)

--
James Kanze
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
Andre Heinen
On 20 Oct 2004 20:47:58 +0200, James Kanze
wrote:

(En fait, c'est assez rare qu'une classe polymorphique supporte
l'affectation. L'héritage et l'affectation ne vont pas bien ensemble.)


Quand on fait une classe polymorphique, vaut-il mieux interdire
l'affectation, ou mettre un opérateur d'affectation en espérant
que l'utilisateur ne mélangera pas les types?

--
Andre Heinen
My address, rot13-encoded: n qbg urvara ng rhebcrnayvax qbg pbz

Avatar
Falk Tannhäuser
Andre Heinen wrote:
On 20 Oct 2004 20:47:58 +0200, James Kanze
wrote:

(En fait, c'est assez rare qu'une classe polymorphique supporte
l'affectation. L'héritage et l'affectation ne vont pas bien ensemble. )


Quand on fait une classe polymorphique, vaut-il mieux interdire
l'affectation, ou mettre un opérateur d'affectation en espérant
que l'utilisateur ne mélangera pas les types?


Mieux vaut pas trop espérer de l'utilisateur :-)
Si on n'as pas besoin de l'affectation (ce qui est très souvent le cas
pour les classes censées représenter des entités du "monde réel" au sens
large) on l'interdit pour être peinard.
Si on en a besoin, on peut se tourner vers une solution du genre
"lettre - enveloppe", permettant de simuler un changement du type
dynamique de l'objet affecté (chose que le C++ ne supporte pas
directement). Une affectation d'enveloppes est alors implémentée
par un appel à la fonction virtuelle 'lettre* clone(lettre const&)'.

Falk


Avatar
Falk Tannhäuser
Andre Heinen wrote:

On 20 Oct 2004 20:47:58 +0200, James Kanze
wrote:

(En fait, c'est assez rare qu'une classe polymorphique supporte
l'affectation. L'héritage et l'affectation ne vont pas bien ensemble. )


Quand on fait une classe polymorphique, vaut-il mieux interdire
l'affectation, ou mettre un opérateur d'affectation en espérant
que l'utilisateur ne mélangera pas les types?

Mieux vaut pas trop espérer de l'utilisateur :-)

Si on n'a pas besoin de l'affectation (ce qui est très souvent le cas
pour les classes censées représenter des entités du "monde réel" au sens
large) on l'interdit pour être peinard.
Si on en a besoin, on peut se tourner vers une solution du genre
"lettre - enveloppe", permettant de simuler un changement du type
dynamique de l'objet affecté (chose que le C++ ne supporte pas
directement). Une affectation d'enveloppes est alors implémentée
par un appel à la fonction virtuelle 'lettre* lettre::clone() const'.

Falk


Avatar
Andre Heinen
On Thu, 21 Oct 2004 14:26:55 +0200, Falk Tannhäuser
wrote:

Andre Heinen wrote:

Quand on fait une classe polymorphique, vaut-il mieux interdire
l'affectation, ou mettre un opérateur d'affectation en espérant
que l'utilisateur ne mélangera pas les types?

Mieux vaut pas trop espérer de l'utilisateur :-)



Je me rends compte, en me relisant, que le simple choix des mots
de ma question donnait déjà la réponse... :-)

--
Andre Heinen
My address, rot13-encoded: n qbg urvara ng rhebcrnayvax qbg pbz


Avatar
James Kanze
Falk Tannhäuser writes:

|> Andre Heinen wrote:

|> > On 20 Oct 2004 20:47:58 +0200, James Kanze
|> > wrote:

|> >>(En fait, c'est assez rare qu'une classe polymorphique supporte
|> >>l'affectation. L'héritage et l'affectation ne vont pas bien
|> >>ensemble.) |> > Quand on fait une classe polymorphique, vaut-il
|> >>mieux interdire |> > l'affectation, ou mettre un opérateur
|> >>d'affectation en espérant |> > que l'utilisateur ne mélangera pas
|> >>les types?

|> Mieux vaut pas trop espérer de l'utilisateur :-)

On dirait que tu as de l'expérience pratique:-).

--
James Kanze
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
1 2