Bonjour,
Dans le cas où le compilateur ne peut pas générer automatiquement le code de l'affectation de mouvement, peut-on considérer comme une bonne pratique de développement le fait de factoriser le code de l'affectation de mouvement en appelant explicitement le destructeur puis le constructeur de mouvement ?
Voici un exemple très concret où je trouve que cette pratique est pertinente.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 Foo& operator=(Foo&& other) noexcept { if(this != &other) { // allows std::swap<Foo>(x,x) this->Foo::~Foo(); // "Foo::" prevents virtuality if the destructor is virtual. new(this) Foo(std::move(other)); } return *this; }
Admettons qu'une personne code un modèle de classe MovableRaiiFileStream<FileStreamType> avec un membre de type FileStreamType* et qui gère l'ouverture et la fermeture :
Ensuite, admettons que cette personne se dise que ce serait mieux que MovableRaiiFileStream<FileStreamType> soit responsable de gérer la mémoire du FileStreamType et que ce serait bien aussi de garder en mémoire le chemin du fichier. D'ailleurs, ce chemin pourra être affiché en cas d'échec de fermeture du fichier. Le code devient alors :
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 template<class FileStreamType> class MovableRaiiFileStream { private: FileStreamType* m_file; bool invariant() { return m_file != nullptr; } public: MovableRaiiFileStream(FileStreamType& file, const std::string& path) : m_file(&file) { file.open(path); if(!file.is_open()) throw std::runtime_error("Unable to open file \"" + path + "\"."); } MovableRaiiFileStream(const MovableRaiiFileStream& other) = delete; MovableRaiiFileStream& operator=(const MovableRaiiFileStream& other) = delete; MovableRaiiFileStream(MovableRaiiFileStream&& other) noexcept : m_file(other.m_file) { other.m_file = nullptr; } ~MovableRaiiFileStream() noexcept { try { if(m_file) m_file->close(); } catch(...) { try { std::cerr << "Unable to close file."; } catch(...) { } } } MovableRaiiFileStream& operator=(MovableRaiiFileStream&& other) noexcept { if(this != &other) { this->MovableRaiiFileStream::~MovableRaiiFileStream(); new(this) MovableRaiiFileStream(std::move(other)); } return *this; } };
De la première à la deuxième version du code, le code du constructeur de mouvement a été supprimé (car le compilateur pouvait le générer) et celui du destructeur a changé. Mais celui de l'affectation de mouvement est inchangé, grâce à la factorisation.
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 template<class FileStreamType> class MovableRaiiFileStream { private: std::unique_ptr<FileStreamType> m_file; std::string m_path; bool invariant() { return bool(m_file); } public: explicit MovableRaiiFileStream(const std::string& path) : m_file(new FileStreamType(path)), m_path(path) { if(!m_file->is_open()) throw std::runtime_error("Unable to open file \"" + path + "\"."); } MovableRaiiFileStream(MovableRaiiFileStream&& other) noexcept = default; ~MovableRaiiFileStream() noexcept { try { if(m_file) m_file->close(); } catch(...) { try { std::cerr << "Unable to close file \"" << m_path << "\"."; } catch(...) { } } } MovableRaiiFileStream& operator=(MovableRaiiFileStream&& other) noexcept { if(this != &other) { this->MovableRaiiFileStream::~MovableRaiiFileStream(); new(this) MovableRaiiFileStream(std::move(other)); } return *this; } };
Pour l'instant, le principal inconvénient qui me vient à l'esprit à propos de cette pratique, c'est que le code ne sera compréhensible que par ceux qui connaissent le placement new.
Qu'en pensez-vous ?
Partager