
Il y a quelques semaines, la conférence principale du monde C ++ - CPPCON .
Cinq jours consécutifs de 8 heures à 22 heures, il y a eu des rapports. Les programmeurs de toutes confessions ont discuté de l'avenir du C ++, des vélos empoisonnés et ont pensé comment rendre le C ++ plus facile.
Étonnamment, de nombreux rapports ont été consacrés au traitement des erreurs. Des approches bien établies ne vous permettent pas d'atteindre des performances maximales ou peuvent générer des feuilles de code.
Quelles innovations nous attendent en C ++ 2a?
Un peu de théorie
Classiquement, toutes les situations erronées du programme peuvent être divisées en 2 grands groupes:
- Erreurs fatales.
- Erreurs non fatales ou attendues.
Erreurs fatales
Après eux, cela n'a aucun sens de poursuivre l'exécution.
Par exemple, il s'agit de déréférencer un pointeur nul, de passer par la mémoire, de diviser par 0 ou de violer d'autres invariants dans le code. Tout ce qui doit être fait lorsqu'ils surviennent est de fournir un maximum d'informations sur le problème et de terminer le programme.
En c ++ trop il existe déjà suffisamment de moyens pour terminer le programme:
Les bibliothèques commencent même à apparaître pour collecter des données sur les plantages ( 1 , 2 , 3 ).
Erreurs non fatales
Ce sont des erreurs fournies par la logique du programme. Par exemple, des erreurs lors de l'utilisation du réseau, de la conversion d'une chaîne non valide en nombre, etc. L'apparition de telles erreurs dans le programme est dans l'ordre des choses. Pour leur traitement, il existe plusieurs tactiques généralement acceptées en C ++.
Nous en parlerons plus en détail à l'aide d'un exemple simple:
Essayons d'écrire une fonction void addTwo()
utilisant différentes approches de gestion des erreurs.
La fonction doit lire 2 lignes, les convertir en int
et imprimer la somme. Besoin de gérer les erreurs d'E / S, le débordement et la conversion en nombre. Je vais omettre les détails d'implémentation sans intérêt. Nous considérerons 3 approches principales.
1. Exceptions
// // IO std::runtime_error std::string readLine(); // int // std::invalid_argument int parseInt(const std::string& str); // a b // std::overflow_error int safeAdd(int a, int b); void addTwo() { try { std::string aStr = readLine(); std::string bStr = readLine(); int a = parseInt(aStr); int b = parseInt(bStr); std::cout << safeAdd(a, b) << std::endl; } catch(const std::exeption& e) { std::cout << e.what() << std::endl; } }
Les exceptions en C ++ vous permettent de gérer les erreurs de manière centralisée sans
inutiles
,
mais vous devez payer pour cela avec tout un tas de problèmes.
- les frais généraux impliqués dans la gestion des exceptions sont assez importants; vous ne pouvez pas souvent lever d'exceptions.
- il vaut mieux ne pas lever les exceptions des constructeurs / destructeurs et observer RAII.
- par la signature de la fonction, il est impossible de comprendre quelle exception peut sortir de la fonction.
- la taille du fichier binaire augmente en raison d'un code de support d'exception supplémentaire.
2. Codes retour
Approche classique héritée de C.
bool readLine(std::string& str); bool parseInt(const std::string& str, int& result); bool safeAdd(int a, int b, int& result); void processError(); void addTwo() { std::string aStr; int ok = readLine(aStr); if (!ok) { processError(); return; } std::string bStr; ok = readLine(bStr); if (!ok) { processError(); return; } int a = 0; ok = parseInt(aStr, a); if (!ok) { processError(); return; } int b = 0; ok = parseInt(bStr, b); if (!ok) { processError(); return; } int result = 0; ok = safeAdd(a, b, result); if (!ok) { processError(); return; } std::cout << result << std::endl; }
Ça n'a pas l'air très bien?
- Vous ne pouvez pas renvoyer la valeur réelle d'une fonction.
- Il est très facile d'oublier de gérer l'erreur (la dernière fois que vous avez vérifié le code retour de printf?).
- Vous devez écrire du code de gestion des erreurs à côté de chaque fonction. Ce code est plus difficile à lire.
L'utilisation de C ++ 17 et C ++ 2a résoudra tous ces problèmes en séquence.
3. C ++ 17 et nodiscard
L' nodiscard
en C ++ 17.
Si vous le spécifiez avant la déclaration de fonction, l'absence de vérification de la valeur de retour provoquera un avertissement du compilateur.
[[nodiscard]] bool doStuff(); doStuff();
Vous pouvez également spécifier nodiscard
pour une classe, une structure ou une classe enum.
Dans ce cas, l'action d'attribut s'étend à toutes les fonctions qui renvoient des valeurs du type étiqueté nodiscard
.
enum class [[nodiscard]] ErrorCode { Exists, PermissionDenied }; ErrorCode createDir(); /* ... */ createDir();
Je ne fournirai pas de code avec nodiscard
.
C ++ 17 std :: facultatif
En C ++ 17, std::optional<T>
.
Voyons maintenant à quoi ressemble le code.
std::optional<std::string> readLine(); std::optional<int> parseInt(const std::string& str); std::optional<int> safeAdd(int a, int b); void addTwo() { std::optional<std::string> aStr = readLine(); std::optional<std::string> bStr = readLine(); if (aStr == std::nullopt || bStr == std::nullopt){ std::cerr << "Some input error" << std::endl; return; } std::optional<int> a = parseInt(*aStr); std::optional<int> b = parseInt(*bStr); if (!a || !b) { std::cerr << "Some parse error" << std::endl; return; } std::optional<int> result = safeAdd(*a, *b); if (!result) { std::cerr << "Integer overflow" << std::endl; return; } std::cout << *result << std::endl; }
Vous pouvez supprimer les arguments in-out des fonctions et le code deviendra plus propre.
Cependant, nous perdons des informations d'erreur. Il est devenu difficile de savoir quand et ce qui a mal tourné.
Vous pouvez remplacer std::optional
par std::variant<ResultType, ValueType>
.
La signification du code est la même que pour std::optional
, mais plus lourde.
C ++ 2a et std :: attendus
std::expected<ResultType, ErrorType>
- un type de modèle spécial , il tombera probablement dans la norme incomplète la plus proche.
Il a 2 paramètres.
En quoi cela diffère-t-il de la variant
habituelle? Qu'est-ce qui le rend spécial?
std::expected
sera une monade .
Il est proposé de prendre en charge un tas d'opérations sur std::expected
comme sur une monade: map
, catch_error
, bind
, unwrap
, return
and then
.
En utilisant ces fonctions, vous pouvez enchaîner les appels de fonction dans une chaîne.
getInt().map([](int i)return i * 2;) .map(integer_divide_by_2) .catch_error([](auto e) return 0; );
Supposons que nous ayons des fonctions avec le retour de std::expected
.
std::expected<std::string, std::runtime_error> readLine(); std::expected<int, std::runtime_error> parseInt(const std::string& str); std::expected<int, std::runtime_error> safeAdd(int a, int b);
Ci-dessous se trouve uniquement un pseudo-code; il ne peut être forcé de fonctionner dans aucun compilateur moderne.
Vous pouvez essayer d'emprunter à Haskell la syntaxe do pour enregistrer les opérations sur les monades. Pourquoi ne pas lui permettre de le faire:
std::expected<int, std::runtime_error> result = do { auto aStr <- readLine(); auto bStr <- readLine(); auto a <- parseInt(aStr); auto b <- parseInt(bStr); return safeAdd(a, b) }
Certains auteurs suggèrent cette syntaxe:
try { auto aStr = try readLine(); auto bStr = try readLine(); auto a = try parseInt(aStr); auto b = try parseInt(bStr); std::cout result << std::endl; return safeAdd(a, b) } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; return 0; }
Le compilateur convertit automatiquement un tel bloc de code en une séquence d'appels de fonction. Si à un moment donné, la fonction ne retourne pas ce que l'on attend d'elle, la chaîne de calcul se rompra. Oui, et comme type d'erreur, vous pouvez utiliser les types d'exceptions déjà existants dans la norme: std::runtime_error
, std::out_of_range
, etc.
Si vous pouvez bien concevoir la syntaxe, alors std::expected
vous permettra d'écrire du code simple et efficace.
Conclusion
Il n'y a pas de moyen idéal pour gérer les erreurs. Jusqu'à récemment, en C ++, il y avait presque toutes les méthodes possibles de gestion des erreurs, sauf les monades.
En C ++ 2a, toutes les méthodes possibles sont susceptibles d'apparaître.
Que lire et voir sur le sujet
- Proposition réelle .
- Discours sur std :: attendu avec CPPCON .
- Andrei Alexandrescu à propos de std :: attendu en C ++ Russie .
- Discussion plus ou moins récente de la proposition sur Reddit .