Y a-t-il réellement une raison pour laquelle surchargé && et || ne court-circuitez pas?

137

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 boolont ce comportement, pourquoi devrait-il être limité à ce type unique? Y a-t-il un raisonnement technique derrière cela?

iFreilicht
la source
1
@PiotrS. Cette question est probablement la réponse. Je suppose que la norme pourrait définir une nouvelle syntaxe juste à cette fin. Probablement comme operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht
1
@PiotrS .: Considérez logique à trois états: {true, false, nil}. Puisqu'il nil&& x == nilpourrait court-circuiter.
MSalters
1
@MSalters: Réfléchissez std::valarray<bool> a, b, c;, comment imaginez-vous a || b || cêtre court-circuité?
Piotr Skotnicki
4
@PiotrS: Je soutiens qu'il existe au moins un type non booléen pour lequel les courts-circuits ont du sens. Je ne dis pas que le court-circuit a du sens pour chaque type non booléen.
MSalters
3
Personne ne l'a encore mentionné, mais il y a aussi le problème de la rétrocompatibilité. À moins qu'une attention particulière ne soit accordée à la limitation des circonstances dans lesquelles ce court-circuit s'appliquerait, un tel court-circuit pourrait rompre le code existant qui surcharge operator&&ou operator||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.
David Hammen

Réponses:

151

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:

A && B

pourrait être réalisé comme quelque chose d'équivalent moral à:

operator_&& ( A, ()=> B )

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:

  • devrions-nous évaluer l'opérande de droite?
  • si la réponse à ce qui précède était "oui", alors comment combiner les deux opérandes?

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.

class C
{
    // Is this thing "false-ish"? If yes, we can skip computing the right
    // hand size of an &&
    public static bool operator false (C c) { whatever }

    // If we didn't skip the RHS, how do we combine them?
    public static C operator & (C left, C right) { whatever }
    ...

(À part: en fait, trois. C # exige que si l'opérateur falseest fourni, l'opérateur truedoit é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:

C cresult = cleft && cright;

Le compilateur génère du code pour cela comme vous pensiez avoir écrit ce pseudo-C #:

C cresult;
C tempLeft = cleft;
cresult = C.false(tempLeft) ? tempLeft : C.&(tempLeft, cright);

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 :

cresult = C.true(tempLeft) ? tempLeft : C.|(tempLeft , cright);

En définissant les quatre opérateurs - true, false, &et |- C # vous permet de dire non seulement , cleft && crightmais aussi non court-circuit cleft & cright, et aussi if (cleft) if (cright) ..., et , c ? consequence : alternativeet while(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/

Eric Lippert
la source
1
@Deduplicator: Vous pourriez également être intéressé de lire cette question et réponses: stackoverflow.com/questions/5965968/…
Eric Lippert
5
Dans ce cas, je pense que le compromis est plus que justifié. Le truc compliqué est quelque chose dont seul l'architecte d'une bibliothèque de classes doit se préoccuper, et en échange de cette complication, cela rend la consommation de la bibliothèque plus facile et plus intuitive.
Cody Gray
1
@EricLippert Je crois qu'Envision a déclaré qu'il avait vu ce message et qu'il pensait que c'était vous ... puis a vu qu'il avait raison. Il ne disait pas que ce n'était pas your postpertinent. His noticing your distinct writing stylen'est pas pertinent.
WernerCD
5
L'équipe Microsoft n'obtient pas assez de crédit pour (1) faire un effort considérable pour faire ce qu'il faut en C # et (2) faire les choses correctement plus de fois qu'autrement.
codenheim du
2
@Voo: Si vous choisissez d'implémenter une conversion implicite en, boolvous pouvez utiliser &&et ||sans implémenter operator true/falseou operator &/|en C # sans problème. Le problème se pose précisément dans la situation où il n'y a pas de conversion au boolpossible , ou où on n'est pas désiré.
Eric Lippert
43

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||()or operator&&()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:

objectA && objectB

Cela se résume à:

template< typename T >
ClassA.operator&&( T const & objectB )

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 ...

objectA && obectB

fait la même chose, maintenant? Alors pourquoi la surcharge en premier lieu?

DevSolar
la source
7
Eh bien, votre erreur logique est de raisonner dans le langage actuellement défini sur les effets d'un langage défini différemment. dans l'ancien temps, beaucoup de débutants faisaient ça. "constructeur virtuel". il a fallu une quantité démesurée d'explications pour les sortir de cette pensée en boîte. de toute façon, avec le court-circuit des opérateurs intégrés, il y a des garanties sur la non-évaluation des arguments. une telle garantie serait également là pour les surcharges définies par l'utilisateur, si un court-circuit était défini pour elles.
Bravo et hth. - Alf
1
@iFreilicht: J'ai essentiellement dit la même chose que Deduplicator ou Piotr, juste avec des mots différents. J'ai développé un peu le point dans la réponse éditée. C'était beaucoup plus pratique de cette façon, les extensions de langage nécessaires (par exemple lambdas) n'existaient que récemment, et l'avantage aurait été négligeable de toute façon. Les rares fois où les responsables auraient «aimé» quelque chose qui n'était pas déjà fait par les compilateurs, en 1998, cela s'est retourné contre eux. (Voir export.)
DevSolar
9
@iFreilicht: Un boolopé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.
DevSolar
1
Je dois admettre que je ne peux pas penser à un seul. La seule raison pour laquelle le court-circuit existe est qu'il fait gagner du temps pour les opérations sur les booléens et que vous pouvez connaître le résultat d'une expression avant que tous les arguments ne soient évalués. Avec d'autres opérations ET, ce n'est pas le cas, et c'est pourquoi &et &&ne sont pas le même opérateur. Merci de m'avoir aidé à réaliser cela.
iFreilicht
8
@iFreilicht: Plutôt, le but du court-circuit est que le calcul du côté gauche peut établir la vérité d'une condition préalable du côté droit . if (x != NULL && x->foo)nécessite un court-circuit, non pour la vitesse, mais pour la sécurité.
Eric Lippert
26

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:

A operator&&(B b, __lazy C c) {return c;}

// And be called like
exp_b && exp_c;
// or
operator&&(exp_b, exp_c);

Il ressemblerait sous la couverture comme:

template<class Func> A operator&&(B b, Func& f) {auto&& c = f(); return c;}
// With `f` restricted to no-argument functors returning a `C`.

// And the call:
operator&&(exp_b, [&]{return exp_c;});

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.

Déduplicateur
la source
6
@iFreilicht: Une question de la forme "pourquoi la fonctionnalité X n'existe-t-elle pas?" a la même réponse: pour exister, la fonctionnalité doit avoir été pensée, considérée comme une bonne idée, conçue, spécifiée, implémentée, testée, documentée et expédiée à l'utilisateur final. Si l'une de ces choses ne s'est pas produite, aucune fonctionnalité. L'une de ces choses ne s'est pas produite avec votre fonctionnalité proposée; découvrir lequel est un problème de recherche historique; commencez à parler aux membres du comité de conception si vous vous souciez de savoir laquelle de ces choses n'a jamais été faite.
Eric Lippert
1
@EricLippert: Et, selon la raison, répétez jusqu'à ce qu'il soit mis en œuvre: Peut-être que cela a été jugé trop compliqué, et personne n'a pensé à faire une réévaluation. Ou la réévaluation s'est terminée par des raisons de rejet différentes de celles précédemment retenues. (btw: Ajout de l'essentiel de votre commentaire)
Deduplicator
@Deduplicator Avec les modèles d'expression, ni le mot-clé lazy ni les lambdas ne sont requis.
Sumant
En passant, notez que le langage original Algol 68 avait une coercition "procédurale" (ainsi qu'une déprocédation, ce qui signifie appeler implicitement une fonction sans paramètre lorsque le contexte requiert le type résultat plutôt que le type de fonction). Cela signifie qu'une expression de type T dans une position qui nécessite une valeur de type "fonction sans paramètre retournant T" (orthographié " proc T" dans Algol 68) serait implicitement transformée en corps de fonction retournant l'expression donnée (lambda implicite). La fonctionnalité a été supprimée (contrairement à la déprocédation) dans la révision de 1973 du langage.
Marc van Leeuwen
... Pour C ++, une approche similaire pourrait être de déclarer des opérateurs comme &&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.
Marc van Leeuwen
13

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 en bool, et

  • le court-circuit peut être facilement exprimé par d'autres moyens, si nécessaire.


Par exemple, si une classe Ta des opérateurs associés &&et ||, alors l'expression

auto x = a && b || c;

a, bet csont des expressions de type T, peuvent être exprimées par court-circuit comme

auto&& and_arg = a;
auto&& and_result = (and_arg? and_arg && b : and_arg);
auto x = (and_result? and_result : and_result || c);

ou peut-être plus clairement comme

auto x = [&]() -> T_op_result
{
    auto&& and_arg = a;
    auto&& and_result = (and_arg? and_arg && b : and_arg);
    if( and_result ) { return and_result; } else { return and_result || b; }
}();

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:

#include <iostream>
using namespace std;

void say( char const* s ) { cout << s; }

struct S
{
    using Op_result = S;

    bool value;
    auto is_true() const -> bool { say( "!! " ); return value; }

    friend
    auto operator&&( S const a, S const b )
        -> S
    { say( "&& " ); return a.value? b : a; }

    friend
    auto operator||( S const a, S const b )
        -> S
    { say( "|| " ); return a.value? a : b; }

    friend
    auto operator<<( ostream& stream, S const o )
        -> ostream&
    { return stream << o.value; }

};

template< class T >
auto is_true( T const& x ) -> bool { return !!x; }

template<>
auto is_true( S const& x ) -> bool { return x.is_true(); }

#define SHORTED_AND( a, b ) \
[&]() \
{ \
    auto&& and_arg = (a); \
    return (is_true( and_arg )? and_arg && (b) : and_arg); \
}()

#define SHORTED_OR( a, b ) \
[&]() \
{ \
    auto&& or_arg = (a); \
    return (is_true( or_arg )? or_arg : or_arg || (b)); \
}()

auto main()
    -> int
{
    cout << boolalpha;
    for( int a = 0; a <= 1; ++a )
    {
        for( int b = 0; b <= 1; ++b )
        {
            for( int c = 0; c <= 1; ++c )
            {
                S oa{!!a}, ob{!!b}, oc{!!c};
                cout << a << b << c << " -> ";
                auto x = SHORTED_OR( SHORTED_AND( oa, ob ), oc );
                cout << x << endl;
            }
        }
    }
}

Production:

000 -> !! !! || faux
001 -> !! !! || vrai
010 -> !! !! || faux
011 -> !! !! || vrai
100 -> !! && !! || faux
101 -> !! && !! || vrai
110 -> !! && !! vrai
111 -> !! && !! vrai

Ici, chaque !!bang-bang montre une conversion en bool, 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.

Bravo et hth. - Alf
la source
J'aime vos substitutions de court-circuit, en particulier celle ternaire, qui est aussi proche que possible.
iFreilicht
Vous manquez le court-circuit du &&- il faudrait une ligne supplémentaire comme if (!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.
Arne Mertz
@ArneMertz: votre commentaire sur "Missing" est apparemment dénué de sens. le commentaire sur ce dont il s'agit, oui je suis conscient de cela. la conversion en boolest nécessaire pour effectuer un court-circuit.
Bravo et hth. - Alf
@ Cheersandhth.-Alf le commentaire sur le manque était pour la première révision de votre réponse où vous avez court-circuité le ||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.
Arne Mertz
@ArneMertz: OK, re versioning, désolé je suis lent à éditer. En ce qui concerne la restriction, non, c'est le résultat de l'opérateur qui doit être restreint, car il doit être converti boolen afin de vérifier la courte circulation d'autres opérateurs dans l'expression. Par exemple, le résultat de a && bdoit être converti en boolpour vérifier la courte circulation du OU logique dans a && b || c.
Bravo et hth. - Alf
5

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 non x = y && zbooléenne yet zdoit conduire à un appel à une fonction comme X operator&& (Y, Z). Cela signifierait que ce y && zn'est qu'une manière sophistiquée d'écrire operator&&(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' newopérateur qui se traduit par l'appel de la fonction operator newsuivi 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 Yest convétible de X, 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:

X operator&&(Y const& y, Z const& z)
{
  if (shortcircuitCondition(y))
    return shortcircuitEvaluation(y);

  <"Syntax for an evaluation-Point for z here">

  return actualImplementation(y,z);
}

On veut rarement surcharger operator||et operator&&, parce qu'il y a rarement un cas où l'écriture a && best 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&&et operator||- juste pour arriver à la conclusion que ce n'est pas moins d'effort que d'écrire la logique par main.

Arne Mertz
la source
Le coût est-il vraiment si élevé? Le langage de programmation D permet de déclarer des paramètres lazyqui 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 ()); }}
BlackJack
@BlackJack ce paramètre paresseux pourrait être implémenté en acceptant a std::function<B()>, ce qui entraînerait une certaine surcharge. Ou si vous êtes prêt à le faire, faites-le template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}. Et peut-être le surcharger avec le Bparamètre "normal" , pour que l'appelant puisse décider quelle version choisir. La lazysyntaxe peut être plus pratique mais présente un certain compromis en termes de performances.
Arne Mertz
1
L'un des problèmes de std::functionversus lazyest que le premier peut être évalué plusieurs fois. Un paramètre paresseux fooqui est utilisé tel quel n'est foo+fooévalué qu'une seule fois.
MSalters
"l'utilisation de courts-circuits serait limitée aux cas où Y est convétible à X" ... non, il est limité aux cas où Xpeut être calculé sur la Yseule 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».
Mooing Duck
1
@Sumant qu'ils le peuvent. Mais vous pouvez également écrire à la main la logique d'une coutume de court-circuit operator&&. La question n'est pas de savoir si c'est possible, mais pourquoi il n'y a pas de moyen pratique.
Arne Mertz
5

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é lazyet 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 du Exprmodè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 classe Saussi longtemps qu'il offre logical_andet logical_orfonctions libres et il est convertible bool. Le code est en C ++ 14 mais l'idée est également applicable en C ++ 98. Voir l' exemple en direct .

#include <iostream>

struct S
{
  bool val;

  explicit S(int i) : val(i) {}  
  explicit S(bool b) : val(b) {}

  template <class Expr>
  S (const Expr & expr)
   : val(evaluate(expr).val)
  { }

  template <class Expr>
  S & operator = (const Expr & expr)
  {
    val = evaluate(expr).val;
    return *this;
  }

  explicit operator bool () const 
  {
    return val;
  }
};

S logical_and (const S & lhs, const S & rhs)
{
    std::cout << "&& ";
    return S{lhs.val && rhs.val};
}

S logical_or (const S & lhs, const S & rhs)
{
    std::cout << "|| ";
    return S{lhs.val || rhs.val};
}


const S & evaluate(const S &s) 
{
  return s;
}

template <class Expr>
S evaluate(const Expr & expr) 
{
  return expr.eval();
}

struct And 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? logical_and(temp, evaluate(r)) : temp;
  }
};

struct Or 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? temp : logical_or(temp, evaluate(r));
  }
};


template <class Op, class LExpr, class RExpr>
struct Expr
{
  Op op;
  const LExpr &lhs;
  const RExpr &rhs;

  Expr(const LExpr& l, const RExpr & r)
   : lhs(l),
     rhs(r)
  {}

  S eval() const 
  {
    return op(lhs, rhs);
  }
};

template <class LExpr>
auto operator && (const LExpr & lhs, const S & rhs)
{
  return Expr<And, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator && (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<And, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

template <class LExpr>
auto operator || (const LExpr & lhs, const S & rhs)
{
  return Expr<Or, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator || (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<Or, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

std::ostream & operator << (std::ostream & o, const S & s)
{
  o << s.val;
  return o;
}

S and_result(S s1, S s2, S s3)
{
  return s1 && s2 && s3;
}

S or_result(S s1, S s2, S s3)
{
  return s1 || s2 || s3;
}

int main(void) 
{
  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << and_result(S{i}, S{j}, S{k}) << std::endl;

  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << or_result(S{i}, S{j}, S{k}) << std::endl;

  return 0;
}
Sumant
la source
5

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.

Y a-t-il réellement une raison pour laquelle surchargé &&et ||ne pas court-circuit?

Les opérateurs logiques surchargés personnalisés ne sont pas obligés de suivre la logique de ces tables de vérité.

Mais pourquoi perdent-ils ce comportement lorsqu'ils sont surchargés?

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.

Niall
la source
4

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?

nj-ath
la source
Comme mentionné dans les commentaires et dans la réponse @Deduplicators, cela serait possible avec une fonctionnalité de langue supplémentaire. Je sais que ça ne marche pas maintenant. Ma question était de savoir quel est le raisonnement derrière l'absence d'une telle fonctionnalité.
iFreilicht
Eh bien, ce serait certainement une fonctionnalité compliquée, étant donné que nous devons nous aventurer à deviner la définition de l'utilisateur!
nj-ath
Qu'en est-il : (<condition>)après la déclaration de l'opérateur pour spécifier une condition à laquelle le deuxième argument n'est pas évalué?
iFreilicht
@iFreilicht: Vous auriez toujours besoin d'un autre corps de fonction unaire.
MSalters
3

mais les opérateurs pour bool ont ce comportement, pourquoi devrait-il être limité à ce type unique?

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.

bames53
la source
1
Je me demande si on a examiné la question de savoir si les surcharges de &&, ||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.
supercat le