Smart pointer et fuite de mémoire lors de l'instanciation du constructeur

a marqué ce sujet comme résolu.

Bonjour tout le monde,

Aujourd'hui avec le C++ moderne (C++11 et C++14), on incite les développeurs à utiliser les smart pointers (les pointeurs intelligents). Pour ma part je trouve que cela dénature un peu le C++ de base car on a un objet surchargé qui va gérer un autre objet mais… s'ils existent c'est qu'il y a des raisons à cela. L'intérêt de ces "conteneur d'instance" (c'est peut-être mal dit de ma part pour les puristes) est d'éviter les fuites de mémoire à cause des risques d'oublie du mot-clé delete… ma question est "Est-ce que cela gère tous les cas pour autant ?"

Soit l'exemple ci-dessous :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <memory>
using namespace std;

class A {
    public:
    A(){
        // Allocation d'un très gros tableau
    }
};

int main() {
    A* a = new A();
    return 0;
}

1- En instanciant l'objet A on va essayer d'allouer un très gros tableau (ligne 8), il est dit dans la FAQ d'isocpp qu'il suffit de lancer explicitement une exception à partir du constructeur pour qu'il n'y ait pas de fuite de mémoire (lien), mais encore faut-t-il connaitre la raison de son exception : Comment gérer les cas spécifiques où par exemple on alloue un tableau alors qu'il n'y a plus suffisamment d'espace ? Ou tout autre exception de raison inconnue ?


Même exemple avec le main() différent :

1
2
3
4
int main() {
    unique_ptr<A> a(new A());
    return 0;
}

2- Y a-t-il une fuite de mémoire dans cet exemple ? (voir ligne 14)


Reprenons l'exemple précédent et remplaçons juste la ligne 2 par ceci :

1
2
3
4
int main() {
    unique_ptr<A> a = make_unique<A>();
    return 0;
}

3- C'est make_unique qui instancie un objet A dont le constructeur lance une exception. make_unique a l'avantage à faire oublier le mot-clé new au développeur, mais je suis sur que ce n'est pas sa seule raison d'être. Est-ce que make_unique empêche en interne la fuite de mémoire par rapport aux exemples précédents ?

Merci

+0 -0

Pour ma part je trouve que cela dénature un peu le C++ de base car on a un objet surchargé qui va gérer un autre objet mais… s'ils existent c'est qu'il y a des raisons à cela.

Gugelhupf

Pourtant, la gestion automatique de la mémoire et l'encapsulation des responsabilités uniques dans des classes dédiées ne date pas du C++11/14. Et le RAII est justement une des caractéristiques du C++ (et ce qui fait sa force aussi, par rapport au C par exemple). Voir par exemple vector, string, etc.

Les smart ptr ne sont qu'une extension de ce principe. La "seule" nouveauté du C++11 est de proposer une implémentation dans la STL. A mon sens, ne pas utiliser des capsules RAII en C++03 est déjà une erreur (avec ses propres smart ptr ou ceux de boost, peu importe)

1- En instanciant l'objet A on va essayer d'allouer un très gros tableau (ligne 8), il est dit dans la FAQ d'isocpp qu'il suffit de lancer explicitement une exception à partir du constructeur pour qu'il n'y ait pas de fuite de mémoire (lien), mais encore faut-t-il connaitre la raison de son exception : Comment gérer les cas spécifiques où par exemple on alloue un tableau alors qu'il n'y a plus suffisamment d'espace ? Ou tout autre exception de raison inconnue ?

Gugelhupf

Pas sur de comprendre. A est managée par le unique_ptr, A sera donc safe (dans ce cas). Par contre, si tu fais de la gestion manuelle de mémoire DANS A, c'est a toi de t'arranger que la libération de la mémoire est correcte DANS A. unique_ptr ne va pas aller voir ce qu'il se passe dans A pour verifier que les choses sont faites correctement.

Donc utilise aussi des ressources managées dans A et ça sera safe (smart ptr, vector, etc)

2- Y a-t-il une fuite de mémoire dans cet exemple ? (voir ligne 14)

Gugelhupf

Si tu gères mal tes ressources dans A, oui. Sinon, non

3- C'est make_unique qui instancie un objet A dont le constructeur lance une exception. make_unique a l'avantage à faire oublier le mot-clé new au développeur, mais je suis sur que ce n'est pas sa seule raison d'être. Est-ce que make_unique empêche en interne la fuite de mémoire par rapport aux exemples précédents ?

Gugelhupf

Dans le code d'exemple simpliste, non. Et dans la majorité des cas, make_unique n'apporte pas de garanties supplémentaires (on ne fait probablement pas trop souvent plusieurs appels a new dans la même expression). Mais c'est justement les cas moins courant qui posent souvent plus de problèmes. Donc il ne faut pas se priver (a mon sens, si on utilisent du C++11, il faut implémenter une version de make_unique et l'utiliser)

+3 -0

Merci pour le lien @informaticienzero, il est très instructif :D (ps: Tu voulais t'investir sur le D, c'est très bien que tu sois passé au C++ en parallèle ;) ).

Ca fait un bail que je n'avais pas fait du C++, ça me rappel à quel point le C++ est dangereux :

Expressions used as function arguments may generally be evaluated in any order, including interleaved

@gbdivers

Pourtant, la gestion automatique de la mémoire et l'encapsulation des responsabilités uniques dans des classes dédiées ne date pas du C++11/14. Et le RAII est justement une des caractéristiques du C++ (et ce qui fait sa force aussi, par rapport au C par exemple).

Je le sais bien, mais ce n'est pas les smart pointers qu'on t'apprend à l'école… déjà qu'on te dit qu'une chaine de caractère c'est "char*" :pirate: . Sinon le RAII ça ne s'invente pas, les smart pointers si et comme ce n'est inclue dans le standard que depuis récemment le fait de les utiliser ne me parait pas naturel.

@gbdivers

Par contre, si tu fais de la gestion manuelle de mémoire DANS A, c'est a toi de t'arranger que la libération de la mémoire est correcte DANS A.

Je ne pensais pas forcément à utiliser new, mais par exemple un std::array avec une taille beaucoup trop élevée.

J'aurais une nouvelle question à vous poser, toujours en lien avec le sujet d'origine, soit l'exemple suivant :

 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
30
31
32
33
34
35
class C {
    public:
    C(){
        throw string("Je lance une exception car je considère que l'objet en cours n'est pas correct");
    }
};

class B {
    private:
    shared_ptr<C> c;

    public:
    B(){
        c = make_shared<C>();
    }
};

class A {
    private:
    shared_ptr<B> b;

    public:
    A(){
        b = make_shared<B>();
    }
};

int main(){
    try {
        shared_ptr<A> a = make_shared<A>();
    } catch(const string& m) {
        cout<<"zut alors"<<endl;
    }
    return 0;
}

4- => Question : Y a-t-il une fuite de mémoire ? Si oui pourquoi (à quel niveau) et comment l'en empêcher ?

Merci :)

+0 -0

ça me rappel à quel point le C++ est dangereux

mais ce n'est pas les smart pointers qu'on t'apprend à l'école

Ca ne te parait pas contradictoire ? Si une auto-école t'apprend à conduire les yeux fermés ou en tenant le volant avec les pieds, c'est la voiture qui est dangereuse ou l'enseignement que tu as reçu ?

Je ne pensais pas forcément à utiliser new, mais par exemple un std::array avec une taille beaucoup trop élevée.

4- => Question : Y a-t-il une fuite de mémoire ? Si oui pourquoi (à quel niveau) et comment l'en empêcher ?

A mon avis, tu n'as pas compris quand une fuite mémoire se produit. Si je reprends le premier code et que tu utilises new :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class A {
    int p;
    public:
    A(){
        p = new int;
    }
    ~A(){
        delete p;
    }
};

int main() {
    A* a = new A();
    delete a;
    return 0;
}

Dans ce code, même avec une gestion manuelle de la mémoire, il n'y a pas de fuite mémoire. Donc dans les deux codes précédents non plus, en utilisant std::array ou les shared (et même les pointeurs nus, du moment que tu mettes les delete dans les destructeurs)

Donc pour résumer la réponse, non, tes deux code ne produisent pas de fuite mémoire. Mais ce n'est pas du aux pointeurs intelligents.

+0 -0

@gbdivers

Ca ne te parait pas contradictoire ? Si une auto-école t'apprend à conduire les yeux fermés ou en tenant le volant avec les pieds, c'est la voiture qui est dangereuse ou l'enseignement que tu as reçu ?

Si mais c'est comme ça que ça se passe dans la réalité, les smarts pointers ne sont pas présents dans le programme, et puis de ce que j'ai vu jusqu'à présent, l'éducation n'a toujours pas réussi à dépasser le stade de la norme C89 (je croise les doigts pour que ça évolue dans le bon sens).

Je comprends quand il y a une fuite de mémoire : c'est quand on ne fait pas - ou ne trouve pas le moyen de faire - appel à delete pour un new.

+0 -0

A mon avis, tu n'as pas compris quand une fuite mémoire se produit. Si je reprends le premier code et que tu utilises new :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class A {
    int p;
    public:
    A(){
        p = new int;
    }
    ~A(){
        delete p;
    }
};

int main() {
    A* a = new A();
    delete a;
    return 0;
}

Dans ce code, même avec une gestion manuelle de la mémoire, il n'y a pas de fuite mémoire. Donc dans les deux codes précédents non plus, en utilisant std::array ou les shared (et même les pointeurs nus, du moment que tu mettes les delete dans les destructeurs)

Le problème de ce code, c'est qu'il va générer des fuites de mémoire en cas d'exceptions. Typiquement, si tu as un exception dans le constructeur de A après l'allocation de la mémoire, ton int est perdu.

En fait, il suffit que le flot de contrôle passe à côté du delete pour générer une fuite de mémoire. Et en réalité, c'est assez facile : continue et break dans les boucles, exceptions et goto (ok, si tu utilises ça, on peut plus rien faire pour toi ^^ ).

En revanche, tu peux être sûr que les destructeur des objets sur la pile seront appelés, c'est le compilateur qui te le garanti. Donc en utilisant bien les pointeurs intelligents, tu as la garanti de ne pas avoir de fuite de mémoire.

Le problème de ce code, c'est qu'il va générer des fuites de mémoire en cas d'exceptions.

Berdes

Oui, donc pas avec ce code, qui ne peut pas générer d'exception après le new, puisqu'il n'a rien après le new.

Le problème est que Gugelhupf présente un code avec des smart ptr et demande s'il y a des fuites mémoires, alors que ce code ne produit pas de fuites mémoires, même avec des pointeurs nus.

+2 -0

Il y a une fuite de mémoire dans mon exemple n°2, l'instanciation de A va générer une exception, le constructeur de l'unique_ptr n'aura pas le temps d'attraper l'instance :

1
2
3
4
int main() {
    unique_ptr<A> a(new A());
    return 0;
}

5.- Pourquoi cette classe possède un constructeur qui prend en paramètre un pointeur ?

  • Ça ne gère pas les exceptions généré par l'instanciation entre les parenthèses du constructeur.
  • Ça foire le comptage de référence si le pointeur est utilisé avant (le code qui suit compile) :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>
#include <memory>
using namespace std;

class A {};

int main() {
    A* a1 = new A();
    A* a2 = a1;
    unique_ptr<A> a(a2);
    return 0;
}

6-. Vous êtes sur que ces pointeurs intelligents sont fait pour aider le développeur ? Ces constructeurs ne m'ont pas l'air très safe pourtant … Pourquoi ils n'ont pas juste présentés make_unique<T> et make_shared<T> au lieu de ces constructeurs foireux ?

EDIT: Numérotation des questions pour suivre le fil de la discussion.

+0 -0

Il y a une fuite de mémoire dans mon exemple n°2, l'instanciation de A va générer une exception, le constructeur de l'unique_ptr n'aura pas le temps d'attraper l'instance :

1
2
3
4
int main() {
    unique_ptr<A> a(new A());
    return 0;
}

Gugelhupf

C'est bien ce qu'il me semblait, il y a un problème de compréhension. Ce code ne produit pas de fuite mémoire, avec ou sans pointeur intelligent.

Si une allocation échoue, il n'y a pas besoin de la libérer.

1
2
3
4
T::T() { throw; }
...
auto p = new T;
// il n'y a rien a libérer

Un classique :

1
2
3
4
5
6
try {
    auto p = new T; // can throw
    auto q = new T; // can throw
} catch(...) {
    delete p; // pas besoin de delete q
}

5.- Pourquoi cette classe possède un constructeur qui prend en paramètre un pointeur ?

  • Ça ne gère pas les exceptions généré par l'instanciation entre les parenthèses du constructeur.
  • Ça foire le comptage de référence si le pointeur est utilisé avant (le code qui suit compile) :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>
#include <memory>
using namespace std;

class A {};

int main() {
    A* a1 = new A();
    A* a2 = a1;
    unique_ptr<A> a(a2);
    return 0;
}

Gugelhupf

5.- Pour la compatibilité avec des anciennes libs (ie récupérer l'ownership d'une ressource allouée par un autre)

5.a. donc si, pas de problème de ce coté. Si le new échoue, la ressource n'est pas allouée et il n'y a rien a libérer.

5.b. pas de comptage de référence dans unique_ptr.

Le problème de ce code est surtout que le fait d'utiliser des pointeurs non managées (a1, a2) en plus du pointeur intelligent (a) est que l'on a toujours un risque que quelqu'un utilise les pointeurs nus, les exporte hors de la fonction… et fini par faire un delete, utiliser une ressource qui a été libérer ou créer un nouveau pointeur intelligent dessus.

Mais le risque et moindre qu'avec shared_ptr, d'ou le fait que make_unique n'a pas été ajouté dans le C++11, amis dans 14.

Le probleme des new avec unique_ptr (donné dans l'article de Sutter), c'est cela :

1
2
3
f(unique_ptr<A> a, unique_ptr<A> b);
...
f(new A, new A);

Si l'un des new échoue avant l'autre, pas de problème, il n'y a pas de fuite mémoire. Par contre, si l'un des new échoue après l'autre, la ressource allouée par le premier new peut ne pas encore être managée et donc ne sera pas libérée. Ie, si on a la séquence suivante :

1
2
3
4
5
new A
new A 
unique_ptr(A*)
unique_ptr(A*)
f(...)

6-. Vous êtes sur que ces pointeurs intelligents sont fait pour aider le développeur ? Ces constructeurs ne m'ont pas l'air très safe pourtant … Pourquoi ils n'ont pas juste présentés make_unique<T> et make_shared<T> au lieu de ces constructeurs foireux ?

Gugelhupf

Donc ils sont safe.

+1 -0

Un classique :

1
2
3
4
5
6
try {
    auto p = new T; // can throw
    auto q = new T; // can throw
} catch(...) {
    delete q; // pas besoin de delete p
}

gbdivers

Au contraire, il faut absolument un delete p c'est le delete q qui est inutile puisque s'il y a une exception alors q n'est pas construit ;) .

+0 -0

Juste pour rebondir sur l'exemple présenté par Gugelhupf :

1
2
3
4
5
6
try {
    auto p = new T; // can throw
    auto q = new T; // can throw
} catch(...) {
    delete q; // pas besoin de delete p
}

Gugelhupf

Ce code montre bien l'intérêt des pointeurs managés puisque la version suivante ne nécessite pas de try/catch pour éviter les fuites de mémoire :

1
2
std::unique_ptr<T> p(new T);
std::unique_ptr<T> q(new T);

Si une exception est généré dans le deuxième constructeur, on sort du bloc contenant p et q, ce qui force l'appel du destructeur de p (q n'étant pas encore déclaré à ce moment là), ce qui libère la mémoire lié au premier constructeur.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte