Bonjour,
Suite à cette discussion, je me pose des questions sur l'intérêt de wrapper des ressources à usage purement interne (à condition de les gérer proprement et correctement).
Ne serait-ce pas essayer de tuer une mouche avec un bazooka ? ou tout du moins, un fusil d'assaut ?
Prenons une classe qui contient un pointeur en tant que donnée membre privée, sachant que les données pointées sont complètement gérées par la classe.
Dans les codes postés, je ne m'intéresse qu'audit pointeur.
Ici, tous les constructeurs allouent de la mémoire pour le pointeur, mais si dans certains cas on n'allouait pas de mémoire, le problème serait le même.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74 class une_classe { typedef ... un_type; un_type *m_pointeur; public: une_classe() : m_pointeur() { try { m_pointeur = new un_type(...); (...) } catch (std::exception const&) { reset(); (...) throw; } } une_classe(...) : m_pointeur() { try { m_pointeur = new un_type(...); (...) } catch (std::exception const&) { reset(); (...) throw; } } une_classe(une_classe const&) = delete; une_classe(une_classe&& autre) noexcept : m_pointeur(autre.m_pointeur) { autre.m_pointeur = nullptr; } ~une_classe() { reset(); } une_classe& operator = (une_classe const&) = delete; une_classe& operator = (une_classe&& autre) noexcept { un_type *tmp = autre.m_pointeur; autre.m_pointeur = nullptr; m_pointeur = tmp; return *this; } private: void set(un_type *ptr) noexcept { delete m_pointeur; m_pointeur = ptr; } void reset() noexcept { return set(nullptr); } }; // class une_classe
L'allocation a lieu dans le corps du constructeur pour être sûr que toutes les autres données membres ont été construites, et pour le cas où certains paramètres du constructeur de un_type nécessiteraient certains calculs.
Bien entendu, si une exception survient dans le constructeur après l'allocation, on pensera à libérer la mémoire allouée.
Pas de constructeur par copie (ni d'opérateur d'affection).
Qui serait chargé de libérer la mémoire ?
Si je ne me trompe pas, le déplacement n'est pas possible pour les types POD.
Il faut donc remettre à zéro le pointeur membre de l'objet que l'on veut déplacer.
Le destructeur se charge de libérer la mémoire allouée pour le pointeur, si tel a été le cas.
Pour l'opérateur d'affectation par déplacement, passer par une variable auxiliaire me permet d'éviter l'écueil de l'auto-affectation.
Quant à l'échange, l'implémentation par défaut utilise le constructeur par déplacement et l'opérateur d'affectation par déplacement.
Donc pas besoin d'y toucher.
C'est sûr, j'aurais pu utiliser l'idiome « move-and-swap » :
S'il y a plusieurs données membres qui ne peuvent pas être déplacées simplement, ça simplifie/clarifie le code...
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 une_classe& une_classe::operator = (une_classe&& autre) noexcept { une_classe tmp = std::move(autre); swap(tmp); return *this; } void une_classe::swap(une_classe& autre) { std::swap(m_pointeur, autre.m_pointeur); } namespace std { void swap(une_classe& x, une_classe& y) { return x.swap(y); } } // namespace std
Au fait, pour swap(x, y), il vaut mieux la surcharger ou la spécialiser ?
Voilà.
m_pointeur n'est jamais exposé à l'extérieur.
Les données pointées peuvent éventuellement être exposées, mais en tant que référence (éventuellement constante).
Si chaque modification de la valeur du pointeur se fait via set ou reset, il ne devrait pas y avoir de problème de ressources non libérées, ou de ressources libérées plusieurs fois.
Jusque là, ça vous va ?
Je n'ai rien oublié ?
Je pars du principe que cette classe n'a pas d'ami (la pauvre...), ou que ses amis ont la décence de ne pas toucher (directement) aux données membres.
Bien.
Intéressons-nous maintenant à la même chose implémentée à l'aide d'un pointeur intelligent adéquat.
C'est sûr, c'est plus concis...
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 class une_classe { typedef ... un_type; std::unique_ptr<un_type> m_pointeur; public: une_classe() : m_pointeur(new un_type(...)) {} une_classe(...) : m_pointeur() { m_pointeur.reset(new un_type(...)); } une_classe(une_classe const&) = delete; une_classe(une_classe&&) = default; ~une_classe() = default; une_classe& operator = (une_classe const&) = delete; une_classe& operator = (une_classe&&) = default; }; // class une_classe
Mais pour autant, est-ce que cela apporte quelque chose au code ?
Ça le rend plus clair ? plus lisible ?
Est-ce que ça ne va pas trop faire grossir la structure de données, alourdir l'application ?
Honnêtement, je n'en sais rien.
Il y a certes moins de lignes, mais au moins dans la première version on sait ce que l'on fait.
Et puis dans la seconde, on rajoute des dépendances.
Et vous, qu'en pensez-vous ?
Hum...
Je viens de penser à un truc.
Le « principe de responsabilité unique ».
Est-ce que par hasard utiliser un pointeur intelligent dans ce cas ne permettrait-il pas de respecter ce principe (en tout cas, d'y tendre), alors que sans, il serait violé ?
Partager