Quelle est la contrainte `enable_if` correcte sur le configurateur de transfert parfait?

Retour aux sources de Herb Sutter ! Les bases de la présentation de Modern C ++ à la CppCon ont présenté différentes options pour transmettre des parameters et comparé leurs performances à leur facilité d’écriture / d’enseignement. L’option ‘advanced’ (offrant les meilleures performances dans tous les cas testés, mais trop difficile à écrire pour la plupart des développeurs) était un transfert parfait, avec l’exemple donné (PDF, p. 28) :

class employee { std::ssortingng name_; public: template <class String, class = std::enable_if_t<!std::is_same<std::decay_t, std::ssortingng>::value>> void set_name(Ssortingng &&name) noexcept( std::is_nothrow_assignable::value) { name_ = std::forward(name); } }; 

L’exemple utilise une fonction de modèle avec une référence de transfert, avec le paramètre de modèle Ssortingng contrainte à l’aide de enable_if . Cependant, la contrainte semble être incorrecte: il semble dire que cette méthode ne peut être utilisée que si le type Ssortingng n’est pas un std::ssortingng , ce qui n’a aucun sens. Cela signifierait que ce membre std::ssortingng peut être défini en utilisant autre chose qu’une valeur std::ssortingng .

 using namespace std::ssortingng_literals; employee e; e.set_name("Bob"s); // error 

Une explication que j’ai considérée était qu’il y avait une faute de frappe simple et que la contrainte était censée être std::is_same<std::decay_t, std::ssortingng>::value au lieu de !std::is_same<std::decay_t, std::ssortingng>::value . Cependant, cela impliquerait que le setter ne fonctionne pas avec, par exemple, const char * et il était évidemment destiné à fonctionner avec ce type étant donné que c’est l’un des cas testés dans la présentation.

Il me semble que la contrainte correcte ressemble plus à:

 template <class String, class = std::enable_if_t<std::is_assignable::value>> void set_name(Ssortingng &&name) noexcept( std::is_nothrow_assignable::value) { name_ = std::forward(name); } 

permettant que tout ce qui peut être assigné au membre soit utilisé avec le setter.

Ai-je la bonne contrainte? Y a-t-il d’autres améliorations à apporter? Existe-t-il une explication à la contrainte initiale, peut-être a-t-elle été sortie de son contexte?


De plus, je me demande si les parties compliquées et «impossibles à lire» de cette déclaration sont vraiment bénéfiques. Puisque nous n’utilisons pas la surcharge, nous pouvons simplement nous fier à l’instanciation de modèle normale:

 template  void set_name(Ssortingng &&name) noexcept( std::is_nothrow_assignable::value) { name_ = std::forward(name); } 

Et bien sûr, il y a un débat sur la question de noexcept si noexcept compte vraiment, certains disant de ne pas trop s’en inquiéter, à l’exception des primitives de mouvement / échange:

 template  void set_name(Ssortingng &&name) { name_ = std::forward(name); } 

Peut-être qu’avec des concepts, il ne serait pas excessivement difficile de contraindre le modèle, simplement pour améliorer les messages d’erreur.

 template  requires std::is_assignable::value void set_name(Ssortingng &&name) { name_ = std::forward(name); } 

Cela aurait tout de même le désavantage de ne pas pouvoir être virtuel et de le placer dans un en-tête (même si, espérons-le, les modules rendront finalement cette décision sans intérêt), mais cela semble assez compréhensible.

Je pense que ce que vous avez est probablement juste, mais dans l’intérêt de ne pas écrire une “réponse” qui est simplement “je suis d’accord”, je proposerai plutôt cette option qui vérifiera l’affectation basée sur les types corrects – qu’il s’agisse de lval, rval, const, peu importe:

 template  auto set_name(Ssortingng&& name) -> decltype(name_ = std::forward(name), void()) { name_ = std::forward(name); } 

Une explication que j’ai considérée était qu’il y avait une faute de frappe simple et que la contrainte était censée être std::is_same, std::ssortingng>::value au lieu de !std::is_same, std::ssortingng>::value .

Oui, la bonne contrainte présentée à l’écran était is_same , pas !is_same . On dirait qu’il y a une faute de frappe sur votre diapositive.


Cependant, cela impliquerait que le poseur ne fonctionne pas, par exemple, const char *

Oui, et je crois que cela a été fait express. Lorsqu’un littéral de chaîne, tel que "foo" est transmis à une fonction acceptant une référence universelle , le type déduit n’est pas un pointeur (car les tableaux ne s’affaiblissent qu’en pointant dans le paramètre template par valeur ). Il s’agit plutôt d’un caractère const char(&)[N] . Cela dit, chaque appel à set_name avec un littéral de chaîne de longueur différente instancierait une nouvelle spécialisation set_name , telle que:

 void set_name(const char (&name)[4]); // set_name("foo"); void set_name(const char (&name)[5]); // set_name("foof"); void set_name(const char (&name)[7]); // set_name("foofoo"); 

La contrainte est supposée définir la référence universelle de manière à accepter et déduire uniquement std::ssortingng types std::ssortingng pour les arguments rvalue ou cv- std::ssortingng& pour les arguments lvalue (c’est pourquoi il s’agit de std::decay ed avant de comparer std::ssortingng dans cette condition std::is_same ).


il était évidemment destiné à fonctionner avec ce type étant donné que c’est l’un des cas testés dans la présentation.

Je pense que la version testée (4) n’était pas contrainte (notez qu’elle s’appelait Ssortingng&& + perfect forwarding), elle pourrait donc être aussi simple que:

 template  void set_name(Ssortingng&& name) { _name = std::forward(name); } 

de sorte que, lorsqu’un littéral de chaîne est passé, il ne construit pas l’instance std::ssortingng avant l’appel de la fonction, comme le feraient les versions non basées sur des modèles (allouer inutilement de la mémoire dans le code de l’appelé pour créer un temporaire std::ssortingng qui être éventuellement déplacé vers une destination éventuellement préallouée comme _name ):

 void set_name(const std::ssortingng& name); void set_name(std::ssortingng&& name); 

Il me semble que la contrainte correcte ressemble davantage à: std::enable_if_t::value>>

Non, comme je l’ai écrit, je ne pense pas que l’intention était de contraindre set_name à accepter les types assignables à std::ssortingng . Encore une fois – ce std::enable_if est là pour avoir une implémentation unique de la fonction set_name prenant une référence universelle acceptant uniquement les valeurs rvalues ​​et lvalues ​​de std::ssortingng (et rien de plus bien que ce soit un template). Dans votre version de std::enable_if transmettre tout ce qui std::enable_if pas assignable à std::ssortingng produirait une erreur, peu importe si la contrainte est ou non lorsque vous tentez de le faire. Notez que nous pourrions éventuellement déplacer simplement l’argument name vers _name s’il s’agit d’ une référence rvalue non constante . La vérification de la possibilité d’atsortingbution est donc inutile, sauf dans les cas où nous n’utilisons pas SFINAE pour exclure cette fonction de la résolution de la surcharge en faveur d’autres surcharges.


Quelle est la contrainte enable_if correcte sur le enable_if de transfert parfait?

 template , std::ssortingng>::value>> 

ou pas de contrainte du tout si cela ne causera aucun impact négatif sur les performances (tout comme le passage de littéraux de chaîne).

J’ai essayé de faire correspondre ces pensées à un commentaire, mais elles ne correspondraient pas. Je présume écrire ceci car je suis mentionné dans les commentaires ci-dessus et dans l’excellent discours de Herb.

Désolé d’être en retard à la fête. J’ai examiné mes notes et en effet je suis coupable de recommander à Herb la contrainte initiale pour l’option 4 moins l’errant ! . Personne n’est parfait, comme le confirmera assurément ma femme, encore moins moi. 🙂

Rappel: le sharepoint Herb (avec lequel je suis d’accord) est de commencer par le simple conseil C ++ 98/03

 set_name(const ssortingng& name); 

et déplacez-vous seulement au besoin. L’option # 4 est un peu de mouvement. Si nous examinons l’option n ° 4, il ne rest plus qu’à comptabiliser les charges, les magasins et les allocations dans une partie critique de l’application. Nous devons assigner name à name_ plus rapidement possible. Si nous sums ici, la lisibilité du code est beaucoup moins importante que la performance, même si la correction est toujours primordiale.

Ne fournir aucune contrainte du tout (pour l’option n ° 4) Je pense être légèrement incorrect. Si un autre code tentait de se contraindre à appeler lui-même employee::set_name , il risquerait d’obtenir la mauvaise réponse si set_name pas du tout contraint:

 template  auto foo(employee& e, Ssortingng&& name) -> decltype(e.set_name(std::forward(name)), void()) { e.set_name(std::forward(name)); // ... 

Si set_name n’est pas contraint et que Ssortingng déduit un type X totalement non lié, la contrainte ci-dessus sur foo inclut de manière incorrecte cette instanciation de foo dans l’ensemble de surcharge. Et la rectitude est toujours roi …

Que faire si nous voulons atsortingbuer un seul caractère à name_ ? Dites A Cela devrait-il être autorisé? Devrait-il être méchant vite?

 e.set_name('A'); 

Eh bien pourquoi pas?! std::ssortingng a juste un tel opérateur d’assignation:

 basic_ssortingng& operator=(value_type c); 

Mais notez qu’il n’y a pas de constructeur correspondant:

 basic_ssortingng(value_type c); // This does not exist 

Par conséquent, is_convertible{} est false , mais is_assignable{} est true .

Ce n’est pas une erreur de logique d’essayer de définir le nom d’une ssortingng avec un caractère (sauf si vous souhaitez append une documentation à l’ employee indiquant). Donc, même si l’implémentation C ++ 98/03 d’origine n’autorisait pas la syntaxe:

 e.set_name('A'); 

Cela permettait la même opération logique de manière moins efficace:

 e.set_name(std::ssortingng(1, 'A')); 

Et nous avons affaire à l’option n ° 4 parce que nous souhaitons optimiser cette solution au maximum.

Pour ces raisons, je pense que is_assignable est le meilleur trait pour contraindre cette fonction. Et stylistiquement, je trouve la technique de Barry pour épeler cette contrainte tout à fait acceptable . C’est donc là que va mon vote.

Notez également que employee et std::ssortingng sont que des exemples dans le discours de Herb. Ce sont des remplaçants pour les types que vous utilisez dans votre code. Ce conseil est destiné à généraliser au code que vous devez traiter.