IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Apprendre à créer votre propre condition d’erreur

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.

1 commentaire Donner une note à l´article (5)

Article lu   fois.

Les deux auteur et traducteur

Traducteur : Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
enum class FlightsErrc
{
  // pas d’erreur 0
  NonexistentLocations = 10, // l’aéroport demandé n’existe pas
  DatesInThePast,            // la réservation est demandée pour hier
  InvertedDates,             // le retour est avant le départ
  NoFlightsFound       = 20, // aucune correspondance n’a été trouvée
  ProtocolViolation    = 30, // par exemple, l’application reçoit un mauvais fichier XML
  ConnectionError,           // l’application n’a pas pu se connecter au serveur
  ResourceError,             // le service est à court de ressources
  Timeout,                   // le service n’a pas répondu dans le délai imparti
};

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 :

 
Sélectionnez
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
enum class SeatsErrc
{
  // pas d’erreur 0
  InvalidRequest = 1,    // par exemple, l’application reçoit un mauvais fichier XML
  CouldNotConnect,       // l’application n’a pas pu se connecter au serveur
  InternalError,         // le service est à court de ressources
  NoResponse,            // le service n’a pas répondu dans le délai imparti
  NonexistentClass,      // la classe demandée n’existe pas
  NoSeatAvailable,       // tous les sièges sont réservés
};

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.

 
Sélectionnez
std::error_code ec2 = SeatsErrc::NoResponse;

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

 
Sélectionnez
1.
2.
3.
4.
if (ec == FlightsErrc::InvertedDates)
  { /* faire quelque chose */ }
else if (ec == SeatsErrc::NoResponse)
  { /* 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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
enum class FailureSource
{
  // pas de catégorie 0
  BadUserInput = 1,
  SystemError = 2,
  NoSolution = 3,
};

namespace std
{
  template <> struct is_error_condition_enum<FailureSource> : true_type {};
}

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) :

 
Sélectionnez
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é :

 
Sélectionnez
1.
2.
3.
std::error_code ec = FlightsErrc::InvertedDates;
assert (ec == FailureSource::BadUserInput);
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
namespace { // espace de noms anonyme

struct FailureSourceCategory: std::error_category
{
  const char* name() const noexcept override;
  std::string message(int ev) const override;
  bool equivalent(const std::error_code& code, int condition) const noexcept override;
};

}

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
const char* FailureSourceCategory::name() const noexcept
{
   return "failure-source";
}

std::string FailureSourceCategory::message(int ev) const
{
  switch (static_cast<FailureSource>(ev))
  {
  case FailureSource::BadUserInput:
    return "la demande de l’utilisateur est invalide";

  case FailureSource::SystemError:
    return "erreur interne";

  case FailureSource::NoSolution:
    return "aucune solution n’a été trouvée pour la demande spécifiée";

  default:
    return "(condition non reconnue)";
  }
}

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
bool FailureSourceCategory::equivalent(
       const std::error_code& ec,
       int cond) const noexcept
{
  switch (static_cast<FailureSource>(cond))
  {
  case FailureSource::BadUserInput:
    return /* TBD */;

  case FailureSource::SystemError:
    return /* TBD */;

  case FailureSource::NoSolution:
    return /* TBD */;

  default:
    return false;
  }
}

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
enum class FlightsErrc
{
  // pas d’erreur 0
  NonexistentLocations = 10, // l’aéroport demandé n’existe pas
  DatesInThePast,            // la réservation est demandée pour hier
  InvertedDates,             // le retour est avant le départ
  NoFlightsFound       = 20, // aucune correspondance n’a été trouvée
  ProtocolViolation    = 30, // par exemple, l’application reçoit un mauvais fichier XML
  ConnectionError,           // l’application n’a pas pu se connecter au serveur
  ResourceError,             // le service est à court de ressources
  Timeout,                   // le service n’a pas répondu dans le délai imparti
};
  • 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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
case FailureSource::BadUserInput:
  // pour SeatsErrc:
  if (ec == SeatsErrc::NonexistentClass)
    return true;

  // pour FlightsErrc
  if (ec.category() == FlightsCat)
    return ec.value() >= 10 && ec.value() < 20;

  // pour n’importe quelle autre énumération de codes d’erreur :
  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.

 
Sélectionnez
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 :

 
Sélectionnez
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.
bool FailureSourceCategory::equivalent(
       const std::error_code& ec,
       int cond) const noexcept
{
  const std::error_category& FlightsCat = std::error_code{FlightsErrc{}}.category();

  switch (static_cast<FailureSource>(cond))
  {
  case FailureSource::BadUserInput:
    if (ec == SeatsErrc::NonexistentClass)
      return true;
    if (ec.category() == FlightsCat)
      return ec.value() >= 10 && ec.value() < 20;
    return false;

  case FailureSource::SystemError:
    if (ec == SeatsErrc::InvalidRequest ||
        ec == SeatsErrc::CouldNotConnect ||
        ec == SeatsErrc::InternalError ||
        ec == SeatsErrc::NoResponse)
      return true;
    if (ec.category() == FlightsCat)
      return ec.value() >= 30 && ec.value() < 40;
    return false;

  case FailureSource::NoSolution:
    if (ec == SeatsErrc::NoSeatAvailable)
      return true;
    if (ec.category() == FlightsCat)
      return ec.value() >= 20 && ec.value() < 30;
    return false;

  default:
    return false;
  }
}

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 :

 
Sélectionnez
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
bool operator==(const error_code& lhs, const error_condition& rhs) noexcept
{
  return lhs.category().equivalent(lhs.value(), rhs) || 
         rhs.category().equivalent(lhs, rhs.value());
}

Et voilà. Maintenant nous pouvons tester nos conditions :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
int main()
{
  std::error_code ec = FlightsErrc::NoFlightsFound;
  assert (ec == FailureSource::NoSolution);
  assert (ec != FailureSource::BadUserInput);

  ec = SeatsErrc::NonexistentClass;
  assert (ec != FailureSource::NoSolution);
  assert (ec == FailureSource::BadUserInput);
}

Pour un exemple complet, voir ici :

 
Cacher/Afficher le codeSélectionnez
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.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
256.
257.
258.
259.
260.
261.
262.
263.
264.
265.
266.
267.
268.
269.
270.
271.
272.
273.
274.
// Ce programme illustre comment insérer son énumération de conditions
// d’erreur au sein d’une infrastructure std::error_condition.
// FILE : flights.h
# include <system_error>

enum class FlightsErrc
{
  // pas d’erreur 0
  NonexistentLocations = 10, // l’aéroport demandé n’existe pas
  DatesInThePast,            // la réservation est demandée pour hier
  InvertedDates,             // le retour est avant le départ
  NoFlightsFound       = 20, // aucune correspondance n’a été trouvée
  ProtocolViolation    = 30, // par exemple, l’application reçoit un mauvais fichier XML
  ConnectionError,           // l’application n’a pas pu se connecter au serveur
  ResourceError,             // le service est à court de ressources
  Timeout,                   // le service n’a pas répondu dans le délai imparti
};

namespace std
{
  template <> struct is_error_code_enum<FlightsErrc> : true_type {};
}

std::error_code make_error_code(FlightsErrc);


// FILE : seats.h
enum class SeatsErrc
{
  // pas d’erreur 0
  InvalidRequest = 1,    // par exemple, l’application reçoit un mauvais fichier XML
  CouldNotConnect,       // l’application n’a pas pu se connecter au serveur
  InternalError,         // le service est à court de ressources
  NoResponse,            // le serveur n’a pas répondu dans le délai imparti
  NonexistentClass,      // la classe demandée n’existe pas
  NoSeatAvailable,       // toutes les places sont réservées
};

namespace std
{
  template <> struct is_error_code_enum<SeatsErrc> : true_type {};
}

std::error_code make_error_code(SeatsErrc);


// FILE : queries.h
// # include "flights.h"
// # include "seats.h"

enum class FailureSource
{
    // pas de catégorie d’erreur pour la valeur 0
    BadUserInput = 1,
    SystemError = 2,
    NoSolution = 3,
};

namespace std
{
  template <> struct is_error_condition_enum<FailureSource> : true_type {};
}

std::error_condition make_error_condition(FailureSource e);


// FILE : main.cpp
// # include "flights.h"
// # include "seats.h"
# include <cassert>
# include <iostream>

int main ()
{
  std::error_code ec = FlightsErrc::NoFlightsFound;
  assert (ec == FailureSource::NoSolution);
  assert (ec != FailureSource::BadUserInput);

  ec = SeatsErrc::NonexistentClass;
  assert (ec != FailureSource::NoSolution);
  assert (ec == FailureSource::BadUserInput);

  std::cout << ec << std::endl;
}


// FILE : flights.cpp
// # include "flights.h"
namespace { // espace de noms anonyme
struct FlightsErrCategory : std::error_category
{
  const char* name() const noexcept override;
  std::string message(int ev) const override;
};

const char* FlightsErrCategory::name() const noexcept
{
  return "vols";
}

std::string FlightsErrCategory::message(int ev) const
{
  switch (static_cast<FlightsErrc>(ev))
  {
  case FlightsErrc::NonexistentLocations:
    return "le nom de l’aéroport dans la demande n’existe pas";

  case FlightsErrc::DatesInThePast:
    return "demande pour une réservation dans le passé";

  case FlightsErrc::InvertedDates:
    return "la date de retour est antérieure à la date d’aller";

  case FlightsErrc::NoFlightsFound:
    return "aucune correspondance n’a été trouvée";

  case FlightsErrc::ProtocolViolation:
    return "la demande reçue est mal formulée";

  case FlightsErrc::ConnectionError:
    return "l’application n’a pas pu se connecter au serveur";

  case FlightsErrc::ResourceError:
    return "ressources insuffisantes";

  case FlightsErrc::Timeout:
    return "le délai imparti pour la demande a expiré";

  default:
    return "(erreur non reconnue)";
  }
}

const FlightsErrCategory theFlightsErrCategory {};

} // espace de noms anonyme

std::error_code make_error_code(FlightsErrc e)
{
  return {static_cast<int>(e), theFlightsErrCategory};
}


// FILE : seats.cpp
// # include "seats.h"
namespace { // espace de noms anonyme

struct SeatsErrCategory : std::error_category
{
  const char* name() const noexcept override;
  std::string message(int ev) const override;
};

const char* SeatsErrCategory::name() const noexcept
{
  return "places";
}

std::string SeatsErrCategory::message(int ev) const
{
  switch (static_cast<SeatsErrc>(ev))
  {
  case SeatsErrc::InvalidRequest:
    return "la demande reçue est mal formulée";

  case SeatsErrc::CouldNotConnect:
    return "l’application n’a pas pu se connecter au serveur";

  case SeatsErrc::InternalError:
    return "ressources insuffisantes";

  case SeatsErrc::NoResponse:
    return "le délai imparti pour la demande a expiré";

  case SeatsErrc::NonexistentClass:
    return "la classe demandée n’existe pas";

  case SeatsErrc::NoSeatAvailable:
    return "toutes les places sont réservées";

  default:
    return "(erreur non reconnue)";
  }
}

const SeatsErrCategory theSeatsErrCategory {};

} // espace de noms anonyme


std::error_code make_error_code(SeatsErrc e)
{
  return {static_cast<int>(e), theSeatsErrCategory};
}


// FILE : queries.cpp
// # include "queries.h"
namespace {

class FailureSourceCategory : public std::error_category
{
public:
  const char* name() const noexcept override;
  std::string message(int ev) const override;
  bool equivalent(const std::error_code& code,int condition) const noexcept override;
};

const char* FailureSourceCategory::name() const noexcept
{
    return "failure-source";
}

std::string FailureSourceCategory::message(int ev) const
{
  switch (static_cast<FailureSource>(ev))
  {
  case FailureSource::BadUserInput:
    return "la demande de l’utilisateur est invalide";

  case FailureSource::SystemError:
    return "erreur interne";

  case FailureSource::NoSolution:
    return "aucune solution n’a été trouvée pour la demande spécifiée";

  default:
    return "(condition non reconnue)";
  }
}

bool FailureSourceCategory::equivalent(const std::error_code& ec, int cond) const noexcept
{
  const std::error_category& FlightsCat = std::error_code{FlightsErrc{}}.category();

  switch (static_cast<FailureSource>(cond))
  {
  case FailureSource::BadUserInput:
    if (ec == SeatsErrc::NonexistentClass)
      return true;
    if (ec.category() == FlightsCat)
      return ec.value() >= 10 && ec.value() < 20;
    return false;

  case FailureSource::SystemError:
    if (ec == SeatsErrc::InvalidRequest ||
        ec == SeatsErrc::CouldNotConnect ||
        ec == SeatsErrc::InternalError ||
        ec == SeatsErrc::NoResponse)
      return true;
    if (ec.category() == FlightsCat)
      return ec.value() >= 30 && ec.value() < 40;
    return false;

  case FailureSource::NoSolution:
    if (ec == SeatsErrc::NoSeatAvailable)
      return true;
    if (ec.category() == FlightsCat)
      return ec.value() >= 20 && ec.value() < 30;
    return false;

  default:
    return false;
  }
}

const FailureSourceCategory theFailureSourceCategory {};

} // espace de noms anonyme

std::error_condition make_error_condition(FailureSource e)
{
  return {static_cast<int>(e), theFailureSourceCategory};
}

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

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 ni 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.