Garanties d’exception et passage par valeur

J’ai récemment rencontré ce problème dans plusieurs contextes, et certaines des opinions exprimées à ce sujet m’ont surpris. Voici un premier exemple simple:

void f(std::vector x) {}; 

La question qui se pose est la suivante: est-il acceptable de documenter ou de décrire f comme offrant la garantie de rejet? De même, je soupçonne que, puisque l’exception n’est pas générée à partir du corps de f, l’utilisation de noexcept est techniquement casher. Mais faut-il le marquer sans noexcept ? Un exemple serait, par exemple, une version optimisée de set qui trouve en quelque sorte utile d’append l’exigence que le comparateur basé sur un modèle ne doit pas lancer. Il le détecte lors de la compilation en utilisant une assertion statique et provoque une erreur. Pourtant, quelqu’un pourrait écrire un comparateur pour les vecteurs prenant par valeur, le marquer sans exception et l’utiliser avec cette version de set . Est-ce alors la faute de l’auteur du conteneur si cela provoque un mauvais comportement? Ou la personne qui a marqué le comparateur noexcept?

Pour prendre un autre exemple impliquant un autre type de garantie d’exception, considérons:

 void g(std::vector x, std::unique_ptr y); 

Cette fonction peut-elle jamais offrir la garantie forte?

 std::vector p{1.0, 2.0}; auto q = std::make_unique(0); bool func_successful = true; try { g(p, std::move(q)); } catch (...) { func_successful = false; } if (!func_successful) assert(q); 

Je dirais que si l’assertion peut échouer, alors g n’offre pas la garantie forte, car q n’était pas vide immédiatement avant d’appeler g (en se rappelant que std::move ne déplace rien en réalité). Et cela peut échouer: l’ordre d’évaluation des parameters n’est pas spécifié, de sorte que y peut être construit d’abord en vidant q, suivi de x en cours de construction et de projection. La fonction en est-elle toujours responsable même si cela ne se produit pas techniquement dans le corps, mais en train d’appeler la fonction?

Edit: J’allais mentionner que Herb Sutter parle de cette question ici: https://youtu.be/xnqTKD8uD64?t=1h11m56s . Il dit que marquer une telle fonction noexcept est “problématique”; J’espère une réponse plus détaillée.

La question qui se pose est la suivante: est-il acceptable de documenter ou de décrire f comme offrant la garantie de rejet?

La norme C ++ ne le croit pas. Considérez son traitement de nothrow lorsqu’il est appliqué à une cession.

is_assignable est vrai si, pour T et U , cette expression est valide: std::declval() = std::declval() . is_nothrow_assignable est true si cette expression est noexcept, dans son intégralité. Il en va de même pour l’affectation copie / déplacement (évidemment avec le même type).

L’initialisation du paramètre operator= fait partie de l’expression. Et donc, si cette initialisation peut lancer, alors l’affectation n’est pas vide. Un document du groupe de travail traite de cette question en profondeur (principalement autour de l’affectation de déménagement).

La question de savoir si vous devez marquer cette fonction noexcept est une question distincte. Mais une fonction ne faisant tout simplement pas noexcept , ne doit pas être confondue avec la conviction que certaines parties d’une expression d’appel de fonction ne seront jamais lancées.

Mon sentiment personnel sur le sujet est que vous ne devriez pas marquer de fonctions aléatoires sans noexcept pour commencer. Vous devriez les utiliser avec des fonctions membres spéciales et d’autres endroits très spécifiques où vous avez explicitement besoin de l’utiliser. C’est-à-dire que cela devrait être plus qu’un marqueur sémantique pour un utilisateur. Utilisez noexcept quand quelqu’un va réellement faire de la métaprogrammation et choisissez d’appeler ou non cette fonction, qu’elle soit noexcept ou noexcept .

La norme (5.2.2 Appel de fonction [expr.call] §4) indique que l’initialisation et la destruction des parameters ont lieu dans le contexte de la fonction appelante. Donc oui, techniquement c’est casher de déclarer f comme non- noexcept .

Cela a toutefois un sens, même si cela est contre-intuitif au début. f pourrait être appelé avec une valeur rvalue, auquel cas move-ctor serait utilisé pour initialiser le paramètre, de sorte que même la construction entière de l’appel de la fonction (y compris la configuration et la noexcept parameters) serait toujours noexcept . Ou f pourrait être appelé avec un vecteur vide, auquel cas l’ensemble de la construction restrait effectivement sans noexcept – du moins avec ce que je considérerais comme des implémentations de bibliothèque “raisonnables”.

Considérons également les fonctions avec les parameters const-ref:

 void x(std::ssortingng const&) noexcept {} void y() noexcept { x("foo"); } 

x n’est noexcept bien. L’appel dans y n’est cependant pas, car il inclut une conversion implicite (éventuellement en jetant). Donc, si déclarer f comme étant noexcept était un mauvais style, ne faudrait-il pas en dire autant de x ?

Donc, je pense que les programmeurs C ++ devraient faire l’une des choses suivantes: ne pas utiliser et ne pas s’appuyer sur des spécifications noexcept , ou internaliser les règles et être toujours las de ce qu’ils font sur le site de l’appel.


Concernant la deuxième question (garantie forte) … Je dirais qu’une fonction ne peut jamais prétendre aux arguments avec lesquels elle est appelée. En outre, il ne peut jamais prétendre au sort de ses parameters potentiels s’il n’est jamais appelé.

Si la garantie forte que vous envisagez inclut de telles réclamations, je conviens que la fonction ne peut pas fournir ce type de garantie forte.

Bien que je ne sois pas nécessairement d’accord pour dire que la garantie forte – pour une fonction portant une signature comme celle-ci – devrait inclure de telles revendications. Ce que je dirais, c’est que: si de telles affirmations sont nécessaires pour que la garantie forte ait un sens pratique quelconque, la fonction ne doit pas utiliser de parameters par valeur.

OTOH, si de telles prétentions ne sont pas nécessaires pour que la forte garantie ait un sens pratique, je ne vois donc aucune raison pour laquelle je “l’interdirais”. Par exemple

 void StrongAppendAll(std::vector& a, std::vector b); 

La meilleure garantie “naturelle” pour cette fonction serait, selon l’OMI, soit d’append tous les éléments de b à a , soit, en cas d’exception, de laisser inchangé . Je ne supposerais jamais que la garantie inclut une revendication sur ce qu’il advient de b . Encore moins toute affirmation sur ce qu’il advient de l’argument utilisé pour initialiser b .