Le comportement de court-circuit des opérateurs &&
et ||
est un outil étonnant pour les programmeurs.
Mais pourquoi perdent-ils ce comportement lorsqu'ils sont surchargés? Je comprends que les opérateurs ne sont que du sucre syntaxique pour les fonctions, mais les opérateurs pour bool
ont ce comportement, pourquoi devrait-il être limité à ce type unique? Y a-t-il un raisonnement technique derrière cela?
operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
{true, false, nil}
. Puisqu'ilnil&& x == nil
pourrait court-circuiter.std::valarray<bool> a, b, c;
, comment imaginez-vousa || b || c
être court-circuité?operator&&
ouoperator||
et dépend des deux opérandes en cours d'évaluation. Le maintien de la compatibilité descendante est (ou devrait être) important lors de l'ajout de fonctionnalités à une langue existante.Réponses:
Tous les processus de conception aboutissent à des compromis entre des objectifs incompatibles entre eux. Malheureusement, le processus de conception de l'
&&
opérateur surchargé en C ++ a produit un résultat final déroutant: la fonctionnalité même que vous attendez&&
- son comportement de court-circuit - est omise.Les détails de la façon dont ce processus de conception s'est terminé dans cet endroit malheureux, ceux que je ne connais pas. Il est cependant pertinent de voir comment un processus de conception ultérieur a pris en compte ce résultat désagréable. En C #, le surcharge
&&
opérateur est court - circuit. Comment les concepteurs de C # ont-ils réalisé cela?Une des autres réponses suggère le "levage lambda". C'est:
pourrait être réalisé comme quelque chose d'équivalent moral à:
où le deuxième argument utilise un mécanisme pour l'évaluation paresseuse de sorte que lorsqu'il est évalué, les effets secondaires et la valeur de l'expression sont produits. L'implémentation de l'opérateur surchargé ne ferait l'évaluation paresseuse que si nécessaire.
Ce n'est pas ce que l'équipe de conception C # a fait. (A part: bien que le levage lambda soit ce que j'ai fait quand est venu le temps de faire une représentation arborescente d'expression de l'
??
opérateur, ce qui nécessite que certaines opérations de conversion soient effectuées paresseusement. Décrire cela en détail serait cependant une digression majeure. Autant dire: levage lambda fonctionne mais est suffisamment lourd pour que nous souhaitions l'éviter.)Au contraire, la solution C # décompose le problème en deux problèmes distincts:
Par conséquent, le problème est résolu en rendant illégale la surcharge
&&
directe. Au contraire, en C #, vous devez surcharger deux opérateurs, dont chacun répond à l'une de ces deux questions.(À part: en fait, trois. C # exige que si l'opérateur
false
est fourni, l'opérateurtrue
doit également être fourni, ce qui répond à la question: est-ce que cette chose est "vraie"? nécessite les deux.)Considérez un énoncé du formulaire:
Le compilateur génère du code pour cela comme vous pensiez avoir écrit ce pseudo-C #:
Comme vous pouvez le voir, le côté gauche est toujours évalué. S'il est déterminé qu'il est "faux-ish", alors c'est le résultat. Sinon, le côté droit est évalué et l' opérateur désireux défini par l'utilisateur
&
est appelé.L'
||
opérateur est défini de manière analogue, comme une invocation de l'opérateur true et de l'|
opérateur impatient :En définissant les quatre opérateurs -
true
,false
,&
et|
- C # vous permet de dire non seulement ,cleft && cright
mais aussi non court-circuitcleft & cright
, et aussiif (cleft) if (cright) ...
, et ,c ? consequence : alternative
etwhile(c)
, et ainsi de suite.Maintenant, j'ai dit que tous les processus de conception sont le résultat de compromis. Ici, les concepteurs de langage C # ont réussi à obtenir un court-circuit
&&
et à||
raison, mais cela nécessite de surcharger quatre opérateurs au lieu de deux , ce que certaines personnes trouvent déroutant. La fonctionnalité d'opérateur vrai / faux est l'une des fonctionnalités les moins bien comprises de C #. L'objectif d'avoir un langage sensé et simple qui est familier aux utilisateurs de C ++ a été opposé par le désir d'avoir des courts-circuits et le désir de ne pas implémenter le levage lambda ou d'autres formes d'évaluation paresseuse. Je pense que c'était une position de compromis raisonnable, mais il est important de se rendre compte qu'il est une position de compromis. Juste un différent position de compromis sur laquelle les concepteurs de C ++ ont atterri.Si le sujet de la conception du langage pour de tels opérateurs vous intéresse, pensez à lire ma série sur les raisons pour lesquelles C # ne définit pas ces opérateurs sur des booléens nullables:
http://ericlippert.com/2012/03/26/null-is-not-false-part-one/
la source
your post
pertinent.His noticing your distinct writing style
n'est pas pertinent.bool
vous pouvez utiliser&&
et||
sans implémenteroperator true/false
ouoperator &/|
en C # sans problème. Le problème se pose précisément dans la situation où il n'y a pas de conversion aubool
possible , ou où on n'est pas désiré.Le fait est que (dans les limites de C ++ 98) l'opérande de droite serait passé à la fonction opérateur surchargée en tant qu'argument. Ce faisant, il serait déjà évalué . Il n'y a rien que le code
operator||()
oroperator&&()
puisse ou ne puisse pas faire pour éviter cela.L'opérateur d'origine est différent, car ce n'est pas une fonction, mais implémenté à un niveau inférieur du langage.
Des fonctionnalités de langage supplémentaires auraient pu rendre syntaxiquement possible la non-évaluation de l'opérande de droite . Cependant, ils ne se sont pas dérangés car il n'y a que quelques cas où cela serait sémantiquement utile. (Tout comme
? :
, qui n'est pas du tout disponible pour la surcharge.(Il leur a fallu 16 ans pour intégrer les lambdas dans la norme ...)
Quant à l'utilisation sémantique, considérez:
Cela se résume à:
Pensez à ce que vous aimeriez faire exactement avec objectB (de type inconnu) ici, autre que d'appeler un opérateur de conversion à
bool
, et comment vous le mettriez en mots pour la définition du langage.Et si vous êtes appelez la conversion à bool, eh bien ...
fait la même chose, maintenant? Alors pourquoi la surcharge en premier lieu?
la source
export
.)bool
opérateur de conversion pour l'une ou l'autre classe a également accès à toutes les variables membres et fonctionne très bien avec l'opérateur intégré. Tout autre chose que la conversion en booléen n'a pas de sens sémantique pour l'évaluation de court-circuit de toute façon! Essayez d'aborder cela d'un point de vue sémantique et non syntaxique: qu'est - ce que vous essayez d'accomplir, pas comment vous le feriez.&
et&&
ne sont pas le même opérateur. Merci de m'avoir aidé à réaliser cela.if (x != NULL && x->foo)
nécessite un court-circuit, non pour la vitesse, mais pour la sécurité.Une fonctionnalité doit être pensée, conçue, implémentée, documentée et expédiée.
Maintenant que nous y avons pensé, voyons pourquoi cela pourrait être facile maintenant (et difficile à faire alors). Gardez également à l'esprit qu'il n'y a qu'une quantité limitée de ressources, donc l'ajouter peut avoir haché autre chose (à quoi aimeriez-vous renoncer?).
En théorie, tous les opérateurs pourraient autoriser un comportement de court-circuit avec une seule fonctionnalité de langage supplémentaire "mineure" , à partir de C ++ 11 (lorsque les lambdas ont été introduits, 32 ans après le lancement de "C avec classes" en 1979, un 16 après c ++ 98):
C ++ aurait juste besoin d'un moyen d'annoter un argument comme évalué paresseux - un lambda caché - pour éviter l'évaluation jusqu'à ce que cela soit nécessaire et autorisé (pré-conditions remplies).
À quoi ressemblerait cette fonctionnalité théorique (rappelez-vous que toute nouvelle fonctionnalité devrait être largement utilisable)?
Une annotation
lazy
, qui s'applique à un argument de fonction, fait de la fonction un modèle attendant un foncteur, et fait que le compilateur compile l'expression dans un foncteur:Il ressemblerait sous la couverture comme:
Veuillez noter que le lambda reste caché et sera appelé au plus une fois.
Il ne devrait y avoir aucune dégradation des performances à cause de cela, mis à part des chances réduites d'élimination de sous-expression commune.
Outre la complexité de l'implémentation et la complexité conceptuelle (chaque fonctionnalité augmente les deux, à moins qu'elle ne facilite suffisamment ces complexités pour certaines autres fonctionnalités), examinons une autre considération importante: la compatibilité descendante.
Bien que cette fonctionnalité de langage ne briserait aucun code, elle modifierait subtilement toute API en tirant parti, ce qui signifie que toute utilisation dans les bibliothèques existantes serait un changement de rupture silencieux.
BTW: Cette fonctionnalité, bien que plus facile à utiliser, est strictement plus puissante que la solution C # de fractionnement
&&
et||
en deux fonctions chacune pour une définition distincte.la source
&&
prendre un argument de type "pointeur vers la fonction retournant T" et une règle de conversion supplémentaire qui permet à une expression d'argument de type T d'être implicitement convertie en une expression lambda. Notez qu'il ne s'agit pas d'une conversion ordinaire, car elle doit se faire au niveau syntaxique: transformer à l'exécution une valeur de type T en une fonction ne serait d'aucune utilité car l'évaluation aurait déjà été effectuée.Avec une rationalisation rétrospective, principalement parce que
afin d'avoir un court-circuit garanti (sans introduire de nouvelle syntaxe), les opérateurs devraient être limités à
résultatsle premier argument réel convertible enbool
, etle court-circuit peut être facilement exprimé par d'autres moyens, si nécessaire.
Par exemple, si une classe
T
a des opérateurs associés&&
et||
, alors l'expressionoù
a
,b
etc
sont des expressions de typeT
, peuvent être exprimées par court-circuit commeou peut-être plus clairement comme
La redondance apparente préserve tous les effets secondaires des invocations d'opérateurs.
Alors que la réécriture lambda est plus verbeuse, sa meilleure encapsulation permet de définir de tels opérateurs.
Je ne suis pas tout à fait sûr de la conformité standard de tout ce qui suit (encore un peu d'influence), mais il se compile proprement avec Visual C ++ 12.0 (2013) et MinGW g ++ 4.8.2:
Production:
Ici, chaque
!!
bang-bang montre une conversion enbool
, c'est- à -dire un contrôle de valeur d'argument.Puisqu'un compilateur peut facilement faire la même chose et l'optimiser en plus, il s'agit d'une implémentation possible démontrée et toute revendication d'impossibilité doit être placée dans la même catégorie que les revendications d'impossibilité en général, à savoir généralement des conneries.
la source
&&
- il faudrait une ligne supplémentaire commeif (!a) { return some_false_ish_T(); }
- et de votre première puce: le court-circuit concerne les paramètres convertibles en booléen, pas les résultats.bool
est nécessaire pour effectuer un court-circuit.||
mais pas le&&
. L'autre commentaire visait le "devrait être limité aux résultats convertibles en booléens" dans votre première puce - il devrait se lire "restreint aux paramètres convertibles en booléens" imo.bool
en afin de vérifier la courte circulation d'autres opérateurs dans l'expression. Par exemple, le résultat dea && b
doit être converti enbool
pour vérifier la courte circulation du OU logique dansa && b || c
.tl; dr : cela ne vaut pas la peine, en raison de la très faible demande (qui utiliserait la fonctionnalité?) par rapport à des coûts plutôt élevés (syntaxe spéciale nécessaire).
La première chose qui me vient à l'esprit est que la surcharge d'opérateurs est juste une façon sophistiquée d'écrire des fonctions, alors que la version booléenne des opérateurs
||
et&&
sont des trucs buitlin. Cela signifie que le compilateur a la liberté de les court-circuiter, tandis que l'expression est nonx = y && z
booléenney
etz
doit conduire à un appel à une fonction commeX operator&& (Y, Z)
. Cela signifierait que cey && z
n'est qu'une manière sophistiquée d'écrireoperator&&(y,z)
qui n'est qu'un appel d'une fonction au nom étrange où les deux paramètres doivent être évalués avant d'appeler la fonction (y compris tout ce qui jugerait un court-circuit approprié).Cependant, on pourrait soutenir qu'il devrait être possible de rendre la traduction des
&&
opérateurs un peu plus sophistiquée, comme c'est le cas pour l'new
opérateur qui se traduit par l'appel de la fonctionoperator new
suivi d'un appel du constructeur.Techniquement ce ne serait pas un problème, il faudrait définir une syntaxe de langage spécifique à la condition préalable qui permet le court-circuit. Cependant, l'utilisation de courts-circuits serait limitée aux cas où il
Y
est convétible deX
, ou bien il devait y avoir des informations supplémentaires sur la façon de faire réellement le court-circuit (c'est-à-dire calculer le résultat à partir du seul premier paramètre). Le résultat devrait ressembler un peu à ceci:On veut rarement surcharger
operator||
etoperator&&
, parce qu'il y a rarement un cas où l'écriturea && b
est réellement intuitive dans un contexte non booléen. Les seules exceptions que je connais sont les modèles d'expression, par exemple pour les DSL intégrés. Et seule une poignée de ces quelques cas bénéficierait d'une évaluation des courts-circuits. Les modèles d'expression ne le font généralement pas, car ils sont utilisés pour former des arbres d'expressions qui sont évalués ultérieurement, vous avez donc toujours besoin des deux côtés de l'expression.En bref: ni les rédacteurs de compilateurs ni les auteurs de normes n'ont ressenti le besoin de sauter à travers les obstacles et de définir et d'implémenter une syntaxe supplémentaire encombrante, simplement parce qu'un sur un million pourrait avoir l'idée qu'il serait bien d'avoir un court-circuit sur l'utilisateur défini
operator&&
etoperator||
- juste pour arriver à la conclusion que ce n'est pas moins d'effort que d'écrire la logique par main.la source
lazy
qui transforment implicitement l'expression donnée en arguments en une fonction anonyme. Cela donne à la fonction appelée le choix d'appeler cet argument ou non. Donc, si le langage a déjà des lambdas, la syntaxe supplémentaire nécessaire est très petite. "Pseudocode": X et (A a, lazy B b) {if (cond (a)) {return short (a); } else {réel (a, b ()); }}std::function<B()>
, ce qui entraînerait une certaine surcharge. Ou si vous êtes prêt à le faire, faites-letemplate <class F> X and(A a, F&& f){ ... actual(a,F()) ...}
. Et peut-être le surcharger avec leB
paramètre "normal" , pour que l'appelant puisse décider quelle version choisir. Lalazy
syntaxe peut être plus pratique mais présente un certain compromis en termes de performances.std::function
versuslazy
est que le premier peut être évalué plusieurs fois. Un paramètre paresseuxfoo
qui est utilisé tel quel n'estfoo+foo
évalué qu'une seule fois.X
peut être calculé sur laY
seule base. Très différent.std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}
. Sauf si vous utilisez un usage très occasionnel de «conversion».operator&&
. La question n'est pas de savoir si c'est possible, mais pourquoi il n'y a pas de moyen pratique.Lambdas n'est pas le seul moyen d'introduire la paresse. L'évaluation paresseuse est relativement simple à l'aide de modèles d'expression en C ++. Il n'y a pas besoin de mot clé
lazy
et il peut être implémenté en C ++ 98. Les arbres d'expression sont déjà mentionnés ci-dessus. Les modèles d'expression sont des arbres d'expression médiocres (mais intelligents). L'astuce consiste à convertir l'expression en un arbre d'instanciations imbriquées récursivement duExpr
modèle. L'arbre est évalué séparément après la construction.Le code suivant met en œuvre à court-circuitées
&&
et les||
opérateurs pour la classeS
aussi longtemps qu'il offrelogical_and
etlogical_or
fonctions libres et il est convertiblebool
. Le code est en C ++ 14 mais l'idée est également applicable en C ++ 98. Voir l' exemple en direct .la source
Le court-circuitage des opérateurs logiques est autorisé car il s'agit d'une "optimisation" dans l'évaluation des tables de vérité associées. C'est une fonction de la logique elle-même, et cette logique est définie.
Les opérateurs logiques surchargés personnalisés ne sont pas obligés de suivre la logique de ces tables de vérité.
Par conséquent, la fonction entière doit être évaluée comme d'habitude. Le compilateur doit le traiter comme un opérateur (ou une fonction) surchargé normal et il peut toujours appliquer des optimisations comme il le ferait avec n'importe quelle autre fonction.
Les gens surchargent les opérateurs logiques pour diverses raisons. Par exemple; ils peuvent avoir une signification spécifique dans un domaine spécifique qui n'est pas la logique «normale» à laquelle les gens sont habitués.
la source
Le court-circuit est dû à la table de vérité de "et" et "ou". Comment sauriez-vous quelle opération l'utilisateur va définir et comment sauriez-vous que vous n'aurez pas à évaluer le deuxième opérateur?
la source
: (<condition>)
après la déclaration de l'opérateur pour spécifier une condition à laquelle le deuxième argument n'est pas évalué?Je veux juste répondre à cette partie. La raison en est que les expressions intégrées
&&
et||
ne sont pas implémentées avec des fonctions comme le sont les opérateurs surchargés.Il est facile d'avoir la logique de court-circuit intégrée à la compréhension du compilateur d'expressions spécifiques. C'est comme n'importe quel autre flux de contrôle intégré.
Mais la surcharge d'opérateurs est implémentée avec des fonctions à la place, qui ont des règles particulières, dont l'une est que toutes les expressions utilisées comme arguments sont évaluées avant l'appel de la fonction. De toute évidence, des règles différentes pourraient être définies, mais c'est un travail plus important.
la source
&&
,||
et,
devraient être autorisés? Le fait que C ++ ne dispose d'aucun mécanisme pour permettre aux surcharges de se comporter comme autre chose que des appels de fonction explique pourquoi les surcharges de ces fonctions ne peuvent rien faire d'autre, mais cela n'explique pas pourquoi ces opérateurs sont surchargeables en premier lieu. Je soupçonne que la vraie raison est simplement qu'ils ont été jetés dans une liste d'opérateurs sans trop y réfléchir.