Apprendre à créer votre propre condition d’erreur

Andrzej Krzemieński

Traducteur

Watchinofoye

Dans le précédent tutoriel, nous avons vu comment créer notre propre énumération de codes d’erreur pour représenter les différentes causes de défaillance d’un composant et comment les stocker et les transmettre, sans considération pour leur type, en utilisant std::error_code. Dans ce tutoriel, nous allons voir comment vérifier une situation d’erreur représentée dans un std::error_code et comment construire des requêtes utiles concernant des conditions d’erreur.

Commentez

- Titre : Apprendre à créer votre propre condition d’erreur

- Auteur : Andrzej Krzemieński

- Traducteur : Watchinofoye

- Parution : 26 avril 2021

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2021 Andrzej Krzemieński. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Rappel

- II. Définir la sémantique pour les conditions

I - Rappel

Souvenons-nous de notre tâche. Dans le tutoriel précédent, nous avions créé une énumération personnalisée de codes d’erreur :

1. enum class FlightsErrc

2. {

3. // pas d’erreur 0

4. NonexistentLocations = 10, // l’aéroport demandé n’existe pas

5. DatesInThePast,            // la réservation est demandée pour hier

6. InvertedDates,             // le retour est avant le départ

7. NoFlightsFound       = 20, // aucune correspondance n’a été trouvée

8. ProtocolViolation    = 30, // par exemple, l’application reçoit un mauvais fichier XML

9. ConnectionError,           // l’application n’a pas pu se connecter au serveur

10. ResourceError,             // le service est à court de ressources

11. Timeout,                   // le service n’a pas répondu dans le délai imparti

12. };

Nous l’avions intégrée au mécanisme de gestion des codes d’erreur du C++, pour que FlightsErrc puisse être interprété comme un std::error_code :

std::error_code ec = FlightsErrc::InvertedDates;

Il s’agit d’erreurs du sous-système responsable de la recherche des correspondances aériennes. Nous avons un autre système pour trouver des sièges vacants à bord des vols trouvés précédemment. Il peut aussi échouer pour de nombreuses raisons :

1. enum class SeatsErrc

2. {

3. // pas d’erreur 0

4. InvalidRequest = 1,    // par exemple, l’application reçoit un mauvais fichier XML

5. CouldNotConnect,       // l’application n’a pas pu se connecter au serveur

6. InternalError,         // le service est à court de ressources

7. NoResponse,            // le service n’a pas répondu dans le délai imparti

8. NonexistentClass,      // la classe demandée n’existe pas

9. NoSeatAvailable,       // tous les sièges sont réservés

10. };

Supposons que nous ayons intégré ces codes d’erreur dans le système de la même manière, en ayant défini une autre catégorie d’erreurs privée, de sorte que cette énumération soit aussi interprétée comme un code d’erreur.

std::error_code ec2 = SeatsErrc::NoResponse;

Nous pourrions, bien sûr, comparer des instances de std::error_code avec nos valeurs énumérées :

1. if (ec == FlightsErrc::InvertedDates)

2. { /* faire quelque chose */ }

3. else if (ec == SeatsErrc::NoResponse)

4. { /* faire autre chose */ }

Ce serait typiquement la chose à ne pas faire. Ce que je souhaite faire avec un std::error_code, c’est soit garder une trace de ses valeurs numériques (sans aucun commentaire), soit être prévenu lorsqu’il se retrouve dans une des conditions suivantes :

1. L’utilisateur nous a transmis une demande illogique ;

2. Il y a un problème avec le système, que l’utilisateur ne comprendra pas forcément, mais qui nous empêche de donner une réponse à sa demande ;

3. Aucune compagnie à notre connaissance n’est en mesure de proposer le trajet demandé.

En ce qui concerne la journalisation, nous l’avons déjà mise en place dans le précédent tutoriel. Concernant la classification, nous pouvons la faire en intégrant une condition d’erreur personnalisée. C’est assez similaire à la mise en place d’un code d’erreur personnalisé : nous allons définir une énumération, puis indiquer à la bibliothèque standard comment l’utiliser en tant que std::error_condition :

1. enum class FailureSource

2. {

3. // pas de catégorie 0

4. BadUserInput = 1,

5. SystemError = 2,

6. NoSolution = 3,

7. };

9. namespace std

10. {

11. template <> struct is_error_condition_enum<FailureSource> : true_type {};

12. }

14. std::error_condition make_error_condition(FailureSource e);

Cette manière de procéder montre deux différences avec la création d’un code d’erreur : nous utilisons le type is_error_condition_enum au lieu de is_error_code_enum ; que nous définissons une conversion vers error_condition plutôt que error_code. Une fois les déclarations ci-dessus mises en place, nous pouvons compiler ce qui suit (mais pas encore faire l’édition de liens) :

std::error_condition cond = FailureSource::BadUserInput;

Ainsi, la bibliothèque vous permet d’enregistrer votre énumération soit comme error_condition soit comme error_code. Les différences entre les deux étant que :

- error_code est utilisé pour stocker et transmettre des codes d’erreur comme s’ils étaient issus de la bibliothèque d’origine et inchangés ;

- error_condition est utilisé pour effectuer des requêtes sur les error_code, dans une optique de regroupement, de classification ou de traduction.

Qu’en est-il de std::error_category ? Vous pouvez voir ça comme un détail technique pour des error_condition et error_code personnalisés.

Vous pouvez tester un error_code avec la condition voulue en utilisant des tests d’égalité :

1. std::error_code ec = FlightsErrc::InvertedDates;

2. assert (ec == FailureSource::BadUserInput);

3. assert (ec != FailureSource::SystemError);

Notre code peut déjà être compilé. Il ne nous reste plus qu’à définir la sémantique, pour que les résultats de comparaisons soient ceux indiqués par les assertions. Vous pouvez utiliser operator==, un peu de manière abusive en ce qui nous concerne, car il ne conserve pas les propriétés des relations d’équivalence, mais c’est ainsi que la vérification des conditions d’erreur est définie dans la bibliothèque standard. Il se peut aussi que ce soit mon utilisation non conventionnelle de la structure d’avoir une condition correspondant à plus d’une valeur de code d’erreur.

II - Définir la sémantique pour les conditions

Afin de définir la correspondance des codes d’erreur par rapport aux conditions, nous devrons définir une autre catégorie dérivée de std::error_category, dans un fichier source .cpp :

1. namespace { // espace de noms anonyme

3. struct FailureSourceCategory: std::error_category

4. {

5. const char* name() const noexcept override;

6. std::string message(int ev) const override;

7. bool equivalent(const std::error_code& code, int condition) const noexcept override;

8. };

10. }

Vous connaissez déjà les deux premières fonctions du précédent tutoriel. La première fournit un nom court pour notre catégorie, la seconde fournit des textes descriptifs pour les valeurs énumérées :

1. const char* FailureSourceCategory::name() const noexcept

2. {

3. return "failure-source";

4. }

6. std::string FailureSourceCategory::message(int ev) const

7. {

8. switch (static_cast<FailureSource>(ev))

9. {

10. case FailureSource::BadUserInput:

11. return "la demande de l’utilisateur est invalide";

13. case FailureSource::SystemError:

14. return "erreur interne";

16. case FailureSource::NoSolution:

17. return "aucune solution n’a été trouvée pour la demande spécifiée";

19. default:

20. return "(condition non reconnue)";

21. }

22. }

La troisième fonction, « equivalent », décrit pour chaque valeur de l’énumération FailureSource la valeur de std::error_code à laquelle elle correspond :

1. bool FailureSourceCategory::equivalent(

2. const std::error_code& ec,

3. int cond) const noexcept

4. {

5. switch (static_cast<FailureSource>(cond))

6. {

7. case FailureSource::BadUserInput:

8. return /* TBD */;

10. case FailureSource::SystemError:

11. return /* TBD */;

13. case FailureSource::NoSolution:

14. return /* TBD */;

16. default:

17. return false;

18. }

19. }

Ici, ec représente le code d’erreur que nous vérifions, et cond est la valeur numérique de notre condition énumérée. La philosophie est que, par défaut, la catégorie n’équivaut pas à un error_code, à moins d’indiquer explicitement que c’est le cas. Cela a des implications pratiques, car ce n’est pas seulement cette fonction qui sera appelée pour déterminer l’équivalence.

Essayons maintenant de définir la sémantique pour la condition FailureSource::BadUserInput. Dans le cas de FlightsErrc, nous pouvons voir que l’énumération n’est pas contiguë – elle a déjà été préparée pour la catégorisation :

1. enum class FlightsErrc

2. {

3. // pas d’erreur 0

4. NonexistentLocations = 10, // l’aéroport demandé n’existe pas

5. DatesInThePast,            // la réservation est demandée pour hier

6. InvertedDates,             // le retour est avant le départ

7. NoFlightsFound       = 20, // aucune correspondance n’a été trouvée

8. ProtocolViolation    = 30, // par exemple, l’application reçoit un mauvais fichier XML

9. ConnectionError,           // l’application n’a pas pu se connecter au serveur

10. ResourceError,             // le service est à court de ressources

11. Timeout,                   // le service n’a pas répondu dans le délai imparti

12. };

- entre 10 et 20 : mauvaise saisie de l’utilisateur ;

- entre 20 et 30 : pas de solution viable ;

- au-dessus de 30 : erreur système interne.

Dans le cas de SeatsErrc, il n’y a pas de telle séparation, nous devons donc énumérer toutes les valeurs intéressantes. Il n’y en a qu’une : SeatsErrc::NonexistentClass.

Notre condition peut alors être écrite ainsi :

1. case FailureSource::BadUserInput:

2. // pour SeatsErrc:

3. if (ec == SeatsErrc::NonexistentClass)

4. return true;

6. // pour FlightsErrc

7. if (ec.category() == FlightsCat)

8. return ec.value() >= 10 && ec.value() < 20;

10. // pour n’importe quelle autre énumération de codes d’erreur :

11. return false;

Pour les valeurs de FlightsErrc, afin d’évaluer des expressions arithmétiques, nous vérifions si la catégorie d’error_code est celle spécifiée par FlightsErrc ; mais, comme nous l’avons appris dans le tutoriel précédent, cette catégorie a été définie comme privée et nous est inaccessible. Nous ne devions pas faire de la catégorie un détail privé, d’autres auteurs suggèrent d’utiliser une fabrique pour retourner l’objet catégorie de notre énumération. Il subsiste toutefois un moyen d’accéder indirectement à cet objet catégorie : créer un error_code dont nous sommes sûrs qu’il stocke un FlightsErrc et accéder à sa catégorie.

const std::error_category& FlightsCat = std::error_code{FlightsErrc{}}.category();

En réalité, la surcharge operator== de std::error_category compare les adresses de l’objet. Maintenant que nous avons obtenu la catégorie, il nous reste seulement à vérifier si la valeur numérique de l’erreur se situe dans l’intervalle voulu.

Définir d’autres cas est similaire et l’entière fonction « equivalent » ressemble à ça :

1. bool FailureSourceCategory::equivalent(

2. const std::error_code& ec,

3. int cond) const noexcept

4. {

5. const std::error_category& FlightsCat = std::error_code{FlightsErrc{}}.category();

7. switch (static_cast<FailureSource>(cond))

8. {

9. case FailureSource::BadUserInput:

10. if (ec == SeatsErrc::NonexistentClass)

11. return true;

12. if (ec.category() == FlightsCat)

13. return ec.value() >= 10 && ec.value() < 20;

14. return false;

16. case FailureSource::SystemError:

17. if (ec == SeatsErrc::InvalidRequest ||

18. ec == SeatsErrc::CouldNotConnect ||

19. ec == SeatsErrc::InternalError ||

20. ec == SeatsErrc::NoResponse)

21. return true;

22. if (ec.category() == FlightsCat)

23. return ec.value() >= 30 && ec.value() < 40;

24. return false;

26. case FailureSource::NoSolution:

27. if (ec == SeatsErrc::NoSeatAvailable)

28. return true;

29. if (ec.category() == FlightsCat)

30. return ec.value() >= 20 && ec.value() < 30;

31. return false;

33. default:

34. return false;

35. }

36. }

Maintenant, vous vous posez peut-être la question suivante : « pourquoi devons-nous définir une énumération puis ces personnalisations et cette catégorie d’erreur, si nous pouvions juste écrire ces trois fonctions is_system_error(), is_bad_input(), is_no_solution() ? » Nous pourrions le faire et cela fonctionnerait pour les énumérations FlightsErrc and SeatsErrc, mais avec aucune autre énumération. Supposons que tout ceci soit une bibliothèque et que quelqu’un d’autre l’utilise : il pourrait créer son propre code d’erreur et vouloir qu’il soit reconnu par ma condition. Le framework C++ pour les codes d’erreur permet cela : vous pouvez apprendre à une nouvelle condition d’erreur à reconnaître des codes d’erreur existants, mais vous pouvez aussi apprendre à un nouveau code d’erreur à être reconnu par des conditions d’erreur existantes. Vous pouvez le faire à travers une fonction membre virtuelle dans std::error_category :

bool equivalent(int code, const error_condition& cond) const noexcept;

Elle a le même nom que la fonction « equivalent », mais utilise un ensemble de paramètres différent. Ainsi, tel que operator== est implémenté entre les codes d’erreur et les conditions d’erreur, il prend les deux surcharges en considération :

1. bool operator==(const error_code& lhs, const error_condition& rhs) noexcept

2. {

3. return lhs.category().equivalent(lhs.value(), rhs) ||

4. rhs.category().equivalent(lhs, rhs.value());

5. }

Et voilà. Maintenant nous pouvons tester nos conditions :

1. int main()

2. {

3. std::error_code ec = FlightsErrc::NoFlightsFound;

4. assert (ec == FailureSource::NoSolution);

5. assert (ec != FailureSource::BadUserInput);

7. ec = SeatsErrc::NonexistentClass;

8. assert (ec != FailureSource::NoSolution);

9. assert (ec == FailureSource::BadUserInput);

10. }

Pour un exemple complet, voir ici :

1. // Ce programme illustre comment insérer son énumération de conditions

2. // d’erreur au sein d’une infrastructure std::error_condition.

3. // FILE : flights.h

4. # include <system_error>

6. enum class FlightsErrc

7. {

8. // pas d’erreur 0

9. NonexistentLocations = 10, // l’aéroport demandé n’existe pas

10. DatesInThePast,            // la réservation est demandée pour hier

11. InvertedDates,             // le retour est avant le départ

12. NoFlightsFound       = 20, // aucune correspondance n’a été trouvée

13. ProtocolViolation    = 30, // par exemple, l’application reçoit un mauvais fichier XML

14. ConnectionError,           // l’application n’a pas pu se connecter au serveur

15. ResourceError,             // le service est à court de ressources

16. Timeout,                   // le service n’a pas répondu dans le délai imparti

17. };

19. namespace std

20. {

21. template <> struct is_error_code_enum<FlightsErrc> : true_type {};

22. }

24. std::error_code make_error_code(FlightsErrc);

27. // FILE : seats.h

28. enum class SeatsErrc

29. {

30. // pas d’erreur 0

31. InvalidRequest = 1,    // par exemple, l’application reçoit un mauvais fichier XML

32. CouldNotConnect,       // l’application n’a pas pu se connecter au serveur

33. InternalError,         // le service est à court de ressources

34. NoResponse,            // le serveur n’a pas répondu dans le délai imparti

35. NonexistentClass,      // la classe demandée n’existe pas

36. NoSeatAvailable,       // toutes les places sont réservées

37. };

39. namespace std

40. {

41. template <> struct is_error_code_enum<SeatsErrc> : true_type {};

42. }

44. std::error_code make_error_code(SeatsErrc);

47. // FILE : queries.h

48. // # include "flights.h"

49. // # include "seats.h"

51. enum class FailureSource

52. {

53. // pas de catégorie d’erreur pour la valeur 0

54. BadUserInput = 1,

55. SystemError = 2,

56. NoSolution = 3,

57. };

59. namespace std

60. {

61. template <> struct is_error_condition_enum<FailureSource> : true_type {};

62. }

64. std::error_condition make_error_condition(FailureSource e);

67. // FILE : main.cpp

68. // # include "flights.h"

69. // # include "seats.h"

70. # include <cassert>

71. # include <iostream>

73. int main ()

74. {

75. std::error_code ec = FlightsErrc::NoFlightsFound;

76. assert (ec == FailureSource::NoSolution);

77. assert (ec != FailureSource::BadUserInput);

79. ec = SeatsErrc::NonexistentClass;

80. assert (ec != FailureSource::NoSolution);

81. assert (ec == FailureSource::BadUserInput);

83. std::cout << ec << std::endl;

84. }

87. // FILE : flights.cpp

88. // # include "flights.h"

89. namespace { // espace de noms anonyme

90. struct FlightsErrCategory : std::error_category

91. {

92. const char* name() const noexcept override;

93. std::string message(int ev) const override;

94. };

96. const char* FlightsErrCategory::name() const noexcept

97. {

98. return "vols";

99. }

101. std::string FlightsErrCategory::message(int ev) const

102. {

103. switch (static_cast<FlightsErrc>(ev))

104. {

105. case FlightsErrc::NonexistentLocations:

106. return "le nom de l’aéroport dans la demande n’existe pas";

108. case FlightsErrc::DatesInThePast:

109. return "demande pour une réservation dans le passé";

111. case FlightsErrc::InvertedDates:

112. return "la date de retour est antérieure à la date d’aller";

114. case FlightsErrc::NoFlightsFound:

115. return "aucune correspondance n’a été trouvée";

117. case FlightsErrc::ProtocolViolation:

118. return "la demande reçue est mal formulée";

120. case FlightsErrc::ConnectionError:

121. return "l’application n’a pas pu se connecter au serveur";

123. case FlightsErrc::ResourceError:

124. return "ressources insuffisantes";

126. case FlightsErrc::Timeout:

127. return "le délai imparti pour la demande a expiré";

129. default:

130. return "(erreur non reconnue)";

131. }

132. }

134. const FlightsErrCategory theFlightsErrCategory {};

136. } // espace de noms anonyme

138. std::error_code make_error_code(FlightsErrc e)

139. {

140. return {static_cast<int>(e), theFlightsErrCategory};

141. }

144. // FILE : seats.cpp

145. // # include "seats.h"

146. namespace { // espace de noms anonyme

148. struct SeatsErrCategory : std::error_category

149. {

150. const char* name() const noexcept override;

151. std::string message(int ev) const override;

152. };

154. const char* SeatsErrCategory::name() const noexcept

155. {

156. return "places";

157. }

159. std::string SeatsErrCategory::message(int ev) const

160. {

161. switch (static_cast<SeatsErrc>(ev))

162. {

163. case SeatsErrc::InvalidRequest:

164. return "la demande reçue est mal formulée";

166. case SeatsErrc::CouldNotConnect:

167. return "l’application n’a pas pu se connecter au serveur";

169. case SeatsErrc::InternalError:

170. return "ressources insuffisantes";

172. case SeatsErrc::NoResponse:

173. return "le délai imparti pour la demande a expiré";

175. case SeatsErrc::NonexistentClass:

176. return "la classe demandée n’existe pas";

178. case SeatsErrc::NoSeatAvailable:

179. return "toutes les places sont réservées";

181. default:

182. return "(erreur non reconnue)";

183. }

184. }

186. const SeatsErrCategory theSeatsErrCategory {};

188. } // espace de noms anonyme

191. std::error_code make_error_code(SeatsErrc e)

192. {

193. return {static_cast<int>(e), theSeatsErrCategory};

194. }

197. // FILE : queries.cpp

198. // # include "queries.h"

199. namespace {

201. class FailureSourceCategory : public std::error_category

202. {

203. public:

204. const char* name() const noexcept override;

205. std::string message(int ev) const override;

206. bool equivalent(const std::error_code& code,int condition) const noexcept override;

207. };

209. const char* FailureSourceCategory::name() const noexcept

210. {

211. return "failure-source";

212. }

214. std::string FailureSourceCategory::message(int ev) const

215. {

216. switch (static_cast<FailureSource>(ev))

217. {

218. case FailureSource::BadUserInput:

219. return "la demande de l’utilisateur est invalide";

221. case FailureSource::SystemError:

222. return "erreur interne";

224. case FailureSource::NoSolution:

225. return "aucune solution n’a été trouvée pour la demande spécifiée";

227. default:

228. return "(condition non reconnue)";

229. }

230. }

232. bool FailureSourceCategory::equivalent(const std::error_code& ec, int cond) const noexcept

233. {

234. const std::error_category& FlightsCat = std::error_code{FlightsErrc{}}.category();

236. switch (static_cast<FailureSource>(cond))

237. {

238. case FailureSource::BadUserInput:

239. if (ec == SeatsErrc::NonexistentClass)

240. return true;

241. if (ec.category() == FlightsCat)

242. return ec.value() >= 10 && ec.value() < 20;

243. return false;

245. case FailureSource::SystemError:

246. if (ec == SeatsErrc::InvalidRequest ||

247. ec == SeatsErrc::CouldNotConnect ||

248. ec == SeatsErrc::InternalError ||

249. ec == SeatsErrc::NoResponse)

250. return true;

251. if (ec.category() == FlightsCat)

252. return ec.value() >= 30 && ec.value() < 40;

253. return false;

255. case FailureSource::NoSolution:

256. if (ec == SeatsErrc::NoSeatAvailable)

257. return true;

258. if (ec.category() == FlightsCat)

259. return ec.value() >= 20 && ec.value() < 30;

260. return false;

262. default:

263. return false;

264. }

265. }

267. const FailureSourceCategory theFailureSourceCategory {};

269. } // espace de noms anonyme

271. std::error_condition make_error_condition(FailureSource e)

272. {

273. return {static_cast<int>(e), theFailureSourceCategory};

274. }