Lors du développement en C ++, vous devez écrire de temps en temps du code dans lequel aucune exception ne devrait se produire. Par exemple, lorsque nous devons écrire un échange sans exception pour les types natifs ou définir une instruction noexcept move pour notre classe, ou implémenter manuellement un destructeur non trivial.
En C ++ 11, le modificateur noexcept a été ajouté au langage, ce qui permet au développeur de comprendre que les exceptions ne peuvent pas être supprimées de la fonction marquée par noexcept. Par conséquent, les fonctions avec une telle marque peuvent être utilisées en toute sécurité dans des contextes où aucune exception ne devrait se produire.
Par exemple, si j'ai ces types et fonctions:
class first_resource {...}; class second_resource {...}; void release(first_resource & r) noexcept; void close(second_resource & r);
et il y a une certaine classe resources_owner
qui possède des objets comme first_resource
et second_resource
:
class resources_owner { first_resource first_resource_; second_resource second_resource_; ... };
alors je peux écrire le destructeur resources_owner
comme suit:
resources_owner::~resources_owner() noexcept {
D'une certaine manière, sauf en C ++ 11 a facilité la vie d'un développeur C ++. Mais l'implémentation noexcept actuelle en C ++ moderne a un côté désagréable ...
Le compilateur n'aide pas à contrôler le contenu des fonctions et méthodes noexcept
Supposons que dans l'exemple ci-dessus je me suis trompé: pour une raison quelconque, j'ai considéré que release()
marqué comme noexcept, mais en réalité il ne l'est pas et peut lever des exceptions. Cela signifie que lorsque j'écris un destructeur à l'aide d'une telle release()
lancement release()
:
resources_owner::~resources_owner() noexcept { release(first_resource_);
alors je prie pour les ennuis. Tôt ou tard, cette release()
lèvera une exception et toute mon application se bloquera en raison du std::terminate()
appelé automatiquement. Ce sera encore pire si mon application ne plante pas, mais celle de quelqu'un d'autre, dans laquelle ils ont utilisé ma bibliothèque avec un destructeur si problématique pour resources_owner
.
Ou une autre variante du même problème. Supposons que je ne me sois pas trompé que release()
effet marqué comme noexcept. Ça l'était.
Il a été balisé dans la version 1.0 d'une bibliothèque tierce à partir de laquelle j'ai pris first_resource
et release()
. Et puis, après plusieurs années, je suis passé à la version 3.0 de cette bibliothèque, mais dans la version 3.0, la release()
n'a plus de modificateur noexcept.
Eh bien quoi? La nouvelle version majeure, ils pourraient facilement casser l'API.
Seulement maintenant, très probablement, j'oublierai de corriger l'implémentation du destructeur resources_owner
. Et si au lieu de moi, quelqu'un d'autre est engagé dans le support de resource_owner
, qui n'a jamais examiné ce destructeur, alors les changements dans la signature release()
passeront probablement inaperçus.
Par conséquent, personnellement, je n'aime pas le fait que le compilateur ne prévienne en aucune façon le programmeur que le programmeur à l'intérieur de la méthode / fonction noexcept lance un appel de méthode / fonction de levée d'exceptions.
Il serait préférable que le compilateur émette de tels avertissements.
Le sauvetage de la noyade est l'œuvre des noyés eux-mêmes
OK, le compilateur ne donne aucun avertissement. Et rien ne peut être fait à propos de ce simple développeur. Ne traitez pas les modifications du compilateur C ++ pour vos propres besoins. Surtout si vous devez utiliser non pas un seul compilateur, mais différentes versions de différents compilateurs C ++.
Est-il possible d'obtenir de l'aide du compilateur sans entrer dans ses abats? C'est-à-dire Est-il possible de faire une sorte d'outils pour contrôler le contenu des méthodes / fonctions noexcept, même si la méthode dendro-fécale?
Tu peux. Sloppy, mais possible.
D'où poussent les jambes?
L'approche décrite dans cet article a été testée dans la pratique lors de la préparation de la prochaine version de notre petit serveur HTTP intégré RESTinio .
Le fait est que RESTinio regorge de fonctionnalités, nous avons perdu de vue les problèmes de sécurité exceptionnels à plusieurs endroits. En particulier, au fil du temps, il est devenu clair que les exceptions peuvent parfois disparaître des rappels transférés à Asio (qui ne devraient pas l'être), ainsi que les exceptions, en principe, peuvent disparaître lors du nettoyage des ressources.
Heureusement, dans la pratique, ces problèmes ne se sont jamais manifestés, mais la dette technique s'est accumulée et il a fallu faire quelque chose. Et vous deviez faire quelque chose avec le code qui était déjà écrit. C'est-à-dire le code non-noexcept de travail doit être converti en code noexcept de travail.
Cela a été fait à l'aide de plusieurs macros, organisées par code aux bons endroits. Par exemple, un cas trivial:
template< typename Message_Builder > void trigger_error_and_close( Message_Builder msg_builder ) noexcept {
Et voici un fragment moins trivial:
void reset() noexcept { RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.empty()); RESTINIO_STATIC_ASSERT_NOEXCEPT( m_context_table.pop_response_context_nonchecked()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front().dequeue_group()); RESTINIO_STATIC_ASSERT_NOEXCEPT(make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed)); for(; !m_context_table.empty(); m_context_table.pop_response_context_nonchecked() ) { const auto ec = make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed ); auto & current_ctx = m_context_table.front(); while( !current_ctx.empty() ) { auto wg = current_ctx.dequeue_group(); restinio::utils::suppress_exceptions_quietly( [&] { wg.invoke_after_write_notificator_if_exists( ec ); } ); } } }
L'utilisation de ces macros s'est serrée la main plusieurs fois, pointant vers des endroits que j'avais par inadvertance perçus comme non, mais qui ne l'étaient pas.
Donc, l'approche décrite ci-dessous, bien sûr, est un lisop fait maison avec des roues carrées, mais ça va ... Je veux dire que cela fonctionne.
Plus loin dans l'article, nous discuterons de l'implémentation qui a été isolée du code RESTinio dans un ensemble distinct de macros.
L'essence de l'approche
L'essence de l'approche est de passer l'instruction / opérateur (stmt), qui doit être vérifié sans exception, dans une certaine macro. Cette macro utilise static_assert(noexcept(stmt), msg)
pour vérifier que stmt est vraiment noexcept, puis remplace stmt dans le code.
Essentiellement, c'est:
ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
sera remplacé par quelque chose comme:
static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
Pourquoi le choix a-t-il été fait en faveur des macros?
En principe, on pourrait se passer de macros et on pourrait écrire static_assert(noexcept(...))
juste dans le code juste avant les actions vérifiées. Mais les macros ont au moins quelques vertus qui font pencher la balance en faveur de l'utilisation spécifique des macros.
Tout d'abord, les macros réduisent la duplication de code. Il y a une comparaison:
static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
et
ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
il est clair qu'avec les macros l'expression principale, c'est-à-dire release(some_resource)
ne peut être écrit qu'une seule fois. Cela réduit la probabilité que le code "rampe" au fil du temps, avec son accompagnement, lorsqu'une correction a été effectuée à un endroit et oubliée au second.
Deuxièmement, les macros et, par conséquent, les contrôles cachés derrière eux peuvent être très facilement désactivés. Disons, si l'abondance de static_assert-s a commencé à affecter négativement la vitesse de compilation (même si je n'ai pas remarqué un tel effet). Ou, plus important encore, lors de la mise à jour d'une bibliothèque tierce, les erreurs de compilation de static_assert cachées derrière des macros peuvent arroser directement avec la rivière. La désactivation temporaire des macros peut permettre une mise à jour fluide du code, y compris des macros de vérification séquentiellement d'abord dans un fichier, puis dans le second, puis dans le troisième, etc.
Ainsi, les macros, bien qu'elles soient obsolètes et très controversées en C ++, dans ce cas particulier, la vie du développeur est simplifiée.
Macro principale ENSURE_NOEXCEPT_STATEMENT
La macro principale ENSURE_NOEXCEPT_STATEMENT est implémentée de manière triviale:
#define ENSURE_NOEXCEPT_STATEMENT(stmt) \ do { \ static_assert(noexcept(stmt), "this statement is expected to be noexcept: " #stmt); \ stmt; \ } while(false)
Il est utilisé pour vérifier que les méthodes / fonctions appelées sont bien noexcept et que leurs appels n'ont pas besoin d'être encadrés par des blocs try-catch. Par exemple:
class some_complex_container { one_container first_data_part_; another_container second_data_part_; ... public: friend void swap(some_complex_container & a, some_complex_container & b) noexcept { using std::swap;
En outre, il existe également la macro ENSURE_NOT_NOEXCEPT_STATEMENT. Il est utilisé pour garantir qu'un bloc try-catch supplémentaire est requis autour de l'appel afin que les exceptions possibles ne surviennent pas:
class some_resource_owner { some_resource resource_; ... public: ~some_resource_owner() noexcept { try {
Macros d'assistance STATIC_ASSERT_NOEXCEPT et STATIC_ASSERT_NOT_NOEXCEPT
Malheureusement, les macros ENSURE_NOEXCEPT_STATEMENT et ENSURE_NOT_NOEXCEPT_STATEMENT ne peuvent être utilisées que pour les instructions / instructions, mais pas pour les expressions qui renvoient une valeur. C'est-à-dire vous ne pouvez pas écrire avec ENSURE_NOEXCEPT_STATEMENT comme ceci:
auto resource = ENSURE_NOEXCEPT_STATEMENT(acquire_resource(params));
Par conséquent, ENSURE_NOEXCEPT_STATEMENT ne peut pas être utilisé, par exemple, dans des boucles où vous devez souvent écrire quelque chose comme:
for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
et vous devez vous assurer que les appels get_first()
, get_next()
, ainsi que l'attribution de nouvelles valeurs pour i ne get_next()
pas d'exception.
Pour lutter contre de telles situations, les macros STATIC_ASSERT_NOEXCEPT et STATIC_ASSERT_NOT_NOEXCEPT ont été écrites, derrière lesquelles seuls les static_assert sont cachés et rien de plus. En utilisant ces macros, je peux obtenir le résultat dont j'ai besoin d'une certaine manière (la compilation de ce fragment particulier n'a pas été vérifiée):
STATIC_ASSERT_NOEXCEPT(something.get_first()); STATIC_ASSERT_NOEXCEPT(something.get_first().get_next()); STATIC_ASSERT_NOEXCEPT(std::declval<decltype(something.get_first())>() = something.get_first().get_next()); for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
De toute évidence, ce n'est pas la meilleure solution, car cela conduit à la duplication du code et augmente le risque de "fluage" avec une maintenance supplémentaire. Mais dans un premier temps, ces macros simples se sont avérées utiles.
Bibliothèque Noexcept-ctcheck
Lorsque j'ai partagé cette expérience sur mon blog et sur Facebook, j'ai reçu une proposition pour organiser les développements ci-dessus dans une bibliothèque séparée. Ce qui a été fait: github a maintenant une petite bibliothèque d'en-tête noexcept-compile-time-check (ou noexcept-ctcheck, si vous économisez sur les lettres) . Donc, tout ce qui précède, vous pouvez prendre et essayer. Certes, les noms des macros sont un peu plus longs que ceux utilisés dans l'article. C'est-à-dire NOEXCEPT_CTCHECK_ENSURE_NOEXCEPT_STATEMENT au lieu de ENSURE_NOEXCEPT_STATEMENT.
Qu'est-ce qui n'est pas entré dans noexcept-ctcheck (encore?)
Il y a un désir de faire la macro ENSURE_NOEXCEPT_EXPRESSION, qui pourrait être utilisée comme ceci:
auto resource = ENSURE_NOEXCEPT_EXPRESSION(acquire_resource(params));
En première approximation, il pourrait ressembler à ceci:
#define ENSURE_NOEXCEPT_EXPRESSION(expr) \ ([&]() noexcept -> decltype(auto) { \ static_assert(noexcept(expr), #expr " is expected to be noexcept"); \ return expr; \ }())
Mais il y a de vagues soupçons qu'il y a des pièges auxquels je n'ai pas pensé. En général, les mains n'ont pas encore atteint ENSURE_NOEXCEPT_EXPRESSION :(
Et si vous rêvez?
Mon vieux rêve est d'obtenir un bloc noexcept en C ++ dans lequel le compilateur lui-même vérifie la levée des exceptions et émet des avertissements si des exceptions peuvent être levées. Il me semble que cela faciliterait l'écriture de code sans exception. Et pas seulement dans les cas évidents mentionnés ci-dessus (swap, move-operators, destructors). Par exemple, un bloc noexcept pourrait aider dans cette situation:
void modify_some_complex_data() {
Ici, pour l'exactitude du code, il est très important que les actions effectuées à l'intérieur des blocs noexcept ne lèvent pas d'exceptions. Et si le compilateur peut tracer cela, alors ce sera une aide sérieuse pour le développeur.
Mais peut-être qu'un bloc noexcept n'est qu'un cas particulier d'un problème plus général. A savoir: vérifier les attentes du programmeur qu'un certain bloc de code possède certaines propriétés. Que ce soit l'absence d'exceptions, l'absence d'effets secondaires, l'absence de récursivité, les courses de données, etc.
Il y a quelques années, des réflexions sur ce sujet ont conduit à l' idée d'attribuer et d'attendre des attributs . Cette idée n’est pas allée plus loin que le billet de blog, car alors qu'elle se trouve loin de mes intérêts et opportunités actuels. Mais soudain, ce sera intéressant pour quelqu'un et quelqu'un va pousser pour créer quelque chose de plus viable.
Conclusion
Dans cet article, j'ai essayé de parler de mon expérience dans la simplification de l'écriture de code sans exception. Bien sûr, l'utilisation de macros ne rend pas le code plus beau et plus compact. Mais ça marche. Et même de telles macros primitives augmentent considérablement le coefficient de mon sommeil réparateur. Donc, si quelqu'un d'autre n'a pas réfléchi à la façon de contrôler le contenu de ses propres méthodes / fonctions noexcept, cet article vous inspirera peut-être à réfléchir à ce sujet.
Et si quelqu'un trouve un moyen de simplifier sa vie en écrivant sans code, alors il serait intéressant de savoir ce qu'est cette méthode, dans laquelle elle aide et dans laquelle elle ne fonctionne pas. Et dans quelle mesure êtes-vous satisfait de ce que vous utilisez?