¿Existe realmente una razón por la que se sobrecargan && y || no cortocircuito?

137

El comportamiento de cortocircuito de los operadores &&y ||es una herramienta increíble para los programadores.

Pero, ¿por qué pierden este comportamiento cuando se sobrecargan? Entiendo que los operadores son simplemente azúcar sintáctica para las funciones, pero los operadores booltienen este comportamiento, ¿por qué debería restringirse a este tipo único? ¿Hay algún razonamiento técnico detrás de esto?

iFreilicht
fuente
1
@PiotrS. Esa pregunta es probablemente la respuesta. Supongo que el estándar podría definir una nueva sintaxis solo para este propósito. Probablemente como operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht
1
@PiotrS .: Considere la lógica de tres estados: {true, false, nil}. Ya nil&& x == nilque podría cortocircuitar.
MSalters
1
@MSalters: Considera std::valarray<bool> a, b, c;, ¿cómo te imaginas a || b || cestar en cortocircuito?
Piotr Skotnicki
44
@PiotrS .: Estoy argumentando que existe al menos un tipo no bool para el cual el corto circuito tiene sentido. No estoy argumentando que el cortocircuito tiene sentido para cada tipo no bool.
MSalters
3
Nadie ha mencionado esto todavía, pero también está el problema de la compatibilidad con versiones anteriores. A menos que se preste especial atención a limitar las circunstancias en las que se aplicaría este cortocircuito, dicho cortocircuito podría romper el código existente que se sobrecarga operator&&o operator||depende de ambos operandos que se evalúan. Mantener la compatibilidad con versiones anteriores es (o debería ser) importante al agregar funciones a un idioma existente.
David Hammen

Respuestas:

151

Todos los procesos de diseño resultan en compromisos entre objetivos mutuamente incompatibles. Desafortunadamente, el proceso de diseño para el &&operador sobrecargado en C ++ produjo un resultado final confuso: que &&se omite la característica que desea , su comportamiento de cortocircuito.

Los detalles de cómo ese proceso de diseño terminó en este desafortunado lugar, aquellos que no conozco. Sin embargo, es relevante ver cómo un proceso de diseño posterior tuvo en cuenta este resultado desagradable. En C #, el &&operador sobrecargado está en cortocircuito. ¿Cómo lograron eso los diseñadores de C #?

Una de las otras respuestas sugiere "levantamiento lambda". Es decir:

A && B

podría realizarse como algo moralmente equivalente a:

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

donde el segundo argumento usa algún mecanismo para la evaluación diferida, de modo que cuando se evalúa, se producen los efectos secundarios y el valor de la expresión. La implementación del operador sobrecargado solo haría la evaluación diferida cuando sea necesario.

Esto no es lo que hizo el equipo de diseño de C #. (Aparte: aunque el levantamiento de lambda es lo que hice cuando llegó el momento de hacer una representación del ??operador del árbol de expresión , lo que requiere que ciertas operaciones de conversión se realicen con pereza. Sin embargo, describir eso en detalle sería una digresión importante. Basta decir: levantamiento de lambda funciona pero es lo suficientemente pesado como para desear evitarlo).

Por el contrario, la solución C # divide el problema en dos problemas separados:

  • ¿Deberíamos evaluar el operando de la derecha?
  • si la respuesta a lo anterior fue "sí", entonces, ¿cómo combinamos los dos operandos?

Por lo tanto, el problema se resuelve al hacer ilegal la sobrecarga &&directa. Por el contrario, en C # debe sobrecargar dos operadores, cada uno de los cuales responde a una de esas dos preguntas.

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

(Aparte: en realidad, tres. C # requiere que si falsese proporciona el operador true, también se debe proporcionar el operador , lo que responde a la pregunta: ¿esto es "verdadero-ish?" requiere ambos.)

Considere una declaración de la forma:

C cresult = cleft && cright;

El compilador genera código para esto como creía que había escrito este pseudo-C #:

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

Como puede ver, el lado izquierdo siempre se evalúa. Si se determina que es "falso-ish", entonces es el resultado. De lo contrario, se evalúa el lado derecho y se invoca al operador ansioso definido por el usuario &.

El ||operador se define de manera análoga, como una invocación del operador verdadero y el |operador ansioso :

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

Mediante la definición de los cuatro operadores - true, false, &y |- C # le permite no sólo de hablar cleft && cright, sino también no cortocircuitos cleft & cright, y también if (cleft) if (cright) ..., y c ? consequence : alternative, y while(c), y así sucesivamente.

Ahora, dije que todos los procesos de diseño son el resultado de un compromiso. Aquí los diseñadores del lenguaje C # consiguieron un cortocircuito &&y la ||derecha, pero hacerlo requiere una sobrecarga cuatro operadores en lugar de dos , que algunas personas encuentran problemas. La función verdadero / falso del operador es una de las características menos conocidas de C #. El objetivo de tener un lenguaje sensible y directo que sea familiar para los usuarios de C ++ se opuso a los deseos de tener un corto circuito y el deseo de no implementar el levantamiento lambda u otras formas de evaluación perezosa. Creo que fue una posición de compromiso razonable, pero es importante darse cuenta de que es una posición de compromiso. Solo un diferente posición de compromiso que los diseñadores de C ++ aterrizaron.

Si el tema del diseño del lenguaje para tales operadores le interesa, considere leer mi serie sobre por qué C # no define estos operadores en booleanos que aceptan valores NULL:

http://ericlippert.com/2012/03/26/null-is-not-false-part-one/

Eric Lippert
fuente
1
@Deduplicator: También te puede interesar leer esta pregunta y respuestas: stackoverflow.com/questions/5965968/…
Eric Lippert
55
En este caso, creo que el compromiso está más que justificado. Lo complicado es algo que solo el arquitecto de una biblioteca de clase debe preocuparse, y a cambio de esta complicación, hace que el consumo de la biblioteca sea más fácil e intuitivo.
Cody Gray
1
@EricLippert Creo que Envision estaba afirmando que vio esta publicación y pensó que eras tú ... luego vio que tenía razón. No decía que your postsea ​​irrelevante. His noticing your distinct writing stylees irrelevante.
WernerCD
55
El equipo de Microsoft no obtiene suficiente crédito por (1) hacer un gran esfuerzo para hacer lo correcto en C # y (2) hacerlo bien la mayoría de las veces.
Codenheim
2
@Voo: si elige implementar una conversión implícita bool, puede usar &&y ||sin implementar operator true/falseo operator &/|en C # no hay problema. El problema surge precisamente en la situación en la que no hay conversión boolposible o donde no se desea.
Eric Lippert
43

El punto es que (dentro de los límites de C ++ 98) el operando de la derecha se pasaría a la función del operador sobrecargado como argumento. Al hacerlo, ya sería evaluado . No hay nada que el operator||()o el operator&&()código pueda o no pueda hacer que evite esto.

El operador original es diferente, porque no es una función, sino que se implementa en un nivel inferior del lenguaje.

Las características adicionales del lenguaje podrían haber hecho que la no evaluación del operando de la mano derecha sea sintácticamente posible . Sin embargo, no se molestaron porque solo hay unos pocos casos selectos en los que esto sería semánticamente útil. (Al igual ? :que, que no está disponible para sobrecargar en absoluto.

(Les tomó 16 años lograr que las lambdas ingresen al estándar ...)

En cuanto al uso semántico, considere:

objectA && objectB

Esto se reduce a:

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

Piense qué es exactamente lo que le gustaría hacer con el objeto B (de tipo desconocido) aquí, aparte de llamar a un operador de conversión bool, y cómo lo pondría en palabras para la definición del lenguaje.

Y si usted está llamando a la conversión a bool, así ...

objectA && obectB

hace lo mismo, ahora lo hace? Entonces, ¿por qué sobrecargar en primer lugar?

DevSolar
fuente
77
bueno, su error lógico es razonar dentro del lenguaje actualmente definido sobre los efectos de un lenguaje definido de manera diferente. en los viejos tiempos, muchos novatos solían hacer eso. "constructor virtual". Se necesitó una cantidad desmesurada de explicaciones para sacarlos de ese pensamiento de caja. de todos modos, con el cortocircuito de los operadores integrados, existen garantías sobre la no evaluación de argumentos. dicha garantía también estaría allí para sobrecargas definidas por el usuario, si se definiera un cortocircuito para ellas.
Saludos y hth. - Alf
1
@iFreilicht: Básicamente dije lo mismo que Deduplicator o Piotr, solo que con diferentes palabras. Elaboré un poco sobre el punto en la respuesta editada. Fue mucho más conveniente de esta manera, las extensiones de lenguaje necesarias (por ejemplo, lambdas) no existían hasta hace poco, y el beneficio habría sido insignificante de todos modos. Las pocas veces en que a las personas responsables les habría "gustado" algo que los constructores de compiladores no habían hecho ya , en 1998, fracasó. (Ver export.)
DevSolar
9
@iFreilicht: un booloperador de conversión para cualquiera de las clases también tiene acceso a todas las variables miembro y funciona bien con el operador incorporado. ¡Cualquier otra cosa que no sea conversión a bool no tiene sentido semántico para la evaluación de cortocircuito de todos modos! Trate de abordar esto desde un punto de vista semántico, no sintáctico: ¿Qué intentaría lograr, no cómo lo haría?
DevSolar
1
Tengo que admitir que no puedo pensar en uno. La única razón por la que existe un cortocircuito es porque ahorra tiempo para las operaciones en booleanos y puede conocer el resultado de una expresión antes de evaluar todos los argumentos. Con otras operaciones AND, ese no es el caso, y ese es el motivo &y &&no son el mismo operador. Gracias por ayudarme a darme cuenta de eso.
iFreilicht
8
@iFreilicht: Más bien, el propósito del cortocircuito es porque el cálculo del lado izquierdo puede establecer la verdad de una precondición del lado derecho . if (x != NULL && x->foo)requiere cortocircuito, no por velocidad, sino por seguridad.
Eric Lippert
26

Una característica tiene que ser pensada, diseñada, implementada, documentada y enviada.

Ahora que lo pensamos, veamos por qué podría ser fácil ahora (y difícil de hacer entonces). También tenga en cuenta que solo hay una cantidad limitada de recursos, por lo que agregarlo podría haber cortado algo más (¿qué le gustaría renunciar a él?).


En teoría, todos los operadores podrían permitir el comportamiento de cortocircuito con solo una característica de lenguaje adicional "menor" , a partir de C ++ 11 (cuando se introdujeron las lambdas, 32 años después de que comenzara "C con clases" en 1979, un 16 todavía respetable después de c ++ 98):

C ++ solo necesitaría una forma de anotar un argumento como evaluado de forma diferida, una lambda oculta, para evitar la evaluación hasta que sea necesario y permitido (se cumplen las condiciones previas).


¿Cómo sería esa característica teórica (recuerde que cualquier característica nueva debería ser ampliamente utilizable)?

Una anotación lazy, que se aplica a un argumento de función, convierte la función en una plantilla que espera un functor, y hace que el compilador empaquete la expresión en un functor:

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

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

Se vería debajo de la cubierta como:

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;});

Tenga en cuenta que la lambda permanece oculta y se llamará a lo sumo una vez.
No debería haber degradación del rendimiento debido a esto, aparte de la reducción de las posibilidades de eliminación de subexpresiones comunes.


Además de la complejidad de implementación y la complejidad conceptual (cada característica aumenta ambas, a menos que alivie lo suficiente esas complejidades para otras características), veamos otra consideración importante: la compatibilidad con versiones anteriores.

Si bien esta función de lenguaje no rompería ningún código, cambiaría sutilmente cualquier API aprovechándola, lo que significa que cualquier uso en las bibliotecas existentes sería un cambio silencioso.

Por cierto: esta característica, aunque es más fácil de usar, es estrictamente más fuerte que la solución C # de división &&y ||en dos funciones cada una para una definición separada.

Deduplicador
fuente
66
@iFreilicht: Cualquier pregunta de la forma "¿por qué no existe la función X?" tiene la misma respuesta: para existir, la característica debe haber sido pensada, considerada una buena idea, diseñada, especificada, implementada, probada, documentada y enviada al usuario final. Si alguna de esas cosas no sucedió, ninguna característica. Una de esas cosas no sucedió con su característica propuesta; averiguar cuál es un problema de investigación histórica; comience a hablar con la gente del comité de diseño si le importa cuál de esas cosas nunca se hizo.
Eric Lippert
1
@EricLippert: Y, según la razón que sea, repita hasta que se implemente: tal vez se pensó demasiado complicado, y nadie pensó en hacer una reevaluación. O la reevaluación terminó con diferentes razones para rechazar que la que se sostuvo anteriormente. (por cierto: se agregó la esencia de tu comentario)
Deduplicator
@Deduplicator Con las plantillas de expresión no se requieren ni la palabra clave perezosa ni lambdas.
Sumant
Como comentario histórico aparte, tenga en cuenta que el lenguaje original de Algol 68 tenía una coerción "procesadora" (además de desprocesamiento, lo que significa llamar implícitamente a una función sin parámetros cuando el contexto requiere el tipo de resultado en lugar del tipo de función). Esto significa que una expresión de tipo T en una posición que requiere un valor de tipo "función sin parámetros que devuelve T" (deletreada " proc T" en Algol 68) se transformaría implícitamente en un cuerpo de función que devuelve la expresión dada (lambda implícita). La característica fue eliminada (a diferencia del desprocesamiento) en la revisión de 1973 del lenguaje.
Marc van Leeuwen
... Para C ++, un enfoque similar podría ser declarar operadores como &&para tomar un argumento de tipo "puntero para funcionar devolviendo T" y una regla de conversión adicional que permite que una expresión de argumento de tipo T se convierta implícitamente en una expresión lambda. Tenga en cuenta que esta no es una conversión ordinaria, ya que debe hacerse a nivel sintáctico: convertir en tiempo de ejecución un valor de tipo T en una función no sería útil ya que la evaluación ya se habría hecho.
Marc van Leeuwen
13

Con racionalización retrospectiva, principalmente porque

  • Para garantizar un cortocircuito garantizado (sin introducir una nueva sintaxis), los operadores tendrían que estar restringidos a resultadosprimer argumento real convertible a bool, y

  • el cortocircuito se puede expresar fácilmente de otras formas, cuando sea necesario.


Por ejemplo, si una clase Ttiene asociados &&y ||operadores, entonces la expresión

auto x = a && b || c;

donde a, by cson expresiones de tipo T, se pueden expresar con cortocircuito como

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

o quizás más claramente como

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 aparente redundancia conserva los efectos secundarios de las invocaciones del operador.


Si bien la reescritura lambda es más detallada, su mejor encapsulación permite definir dichos operadores.

No estoy completamente seguro de la conformidad estándar de todo lo siguiente (todavía un poco de influencia), pero se compila limpiamente con Visual C ++ 12.0 (2013) y 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;
            }
        }
    }
}

Salida:

000 -> !! !! || falso
001 -> !! !! || cierto
010 -> !! !! || falso
011 -> !! !! || cierto
100 -> !! && !! || falso
101 -> !! && !! || cierto
110 -> !! && !! cierto
111 -> !! && !! cierto

Aquí cada !!bang-bang muestra una conversión a bool, es decir, una comprobación de valor de argumento.

Dado que un compilador puede hacer lo mismo fácilmente y, además, optimizarlo, esta es una posible implementación demostrada y cualquier reclamo de imposibilidad debe clasificarse en la misma categoría que los reclamos de imposibilidad en general, es decir, generalmente bollocks.

Saludos y hth. - Alf
fuente
Me gustan sus sustituciones de cortocircuito, especialmente la ternaria, que es lo más cercana que probablemente pueda obtener.
iFreilicht
Te estás perdiendo el cortocircuito del &&- necesitaría una línea adicional como if (!a) { return some_false_ish_T(); }- y a tu primera viñeta: el cortocircuito se trata de los parámetros convertibles a bool, no de los resultados.
Arne Mertz
@ArneMertz: su comentario sobre "Desaparecido" aparentemente no tiene sentido. el comentario sobre de qué se trata, sí, soy consciente de eso. la conversión a booles necesaria para hacer un cortocircuito.
Saludos y hth. - Alf
@ Cheersandhth.-Alf el comentario sobre la falta fue para la primera revisión de su respuesta donde hizo un corto circuito ||pero no el &&. El otro comentario estaba dirigido a "tendría que estar restringido a resultados convertibles a bool" en su primer punto de viñeta - debería leer "restringido a parámetros convertibles a bool" imo.
Arne Mertz
@ArneMertz: OK, re versionando, lo siento, estoy editando lentamente. Re restringido, no, es el resultado del operador el que debe restringirse, ya que tiene que convertirse boolpara verificar el cortocircuito de otros operadores en la expresión. Como, el resultado de a && btiene que convertirse a boolpara comprobar el cortocircuito del OR lógico en a && b || c.
Saludos y hth. - Alf
5

tl; dr : no vale la pena el esfuerzo, debido a la demanda muy baja (¿quién usaría la función?) en comparación con los costos bastante altos (se necesita una sintaxis especial).

Lo primero que viene a la mente es que la sobrecarga del operador es solo una forma elegante de escribir funciones, mientras que la versión booleana de los operadores ||y &&son cosas de buitlin. Eso significa que el compilador tiene la libertad de cortocircuitarlos, mientras que la expresión x = y && zcon no booleano yy ztiene que conducir a una llamada a una función como X operator&& (Y, Z). Esto significaría que y && zes solo una forma elegante de escribir, operator&&(y,z)que es solo una llamada de una función con un nombre extraño donde ambos parámetros deben evaluarse antes de llamar a la función (incluido cualquier cosa que considere un cortocircuito apropiado).

Sin embargo, se podría argumentar que debería ser posible hacer que la traducción de &&operadores sea algo más sofisticada, como lo es para el newoperador que se traduce en llamar a la función operator newseguida de una llamada de constructor.

Técnicamente, esto no sería un problema, habría que definir una sintaxis de lenguaje específica para la condición previa que permita el cortocircuito. Sin embargo, el uso de cortocircuitos se restringiría a los casos en los que Ysea ​​convertible Xo, de lo contrario, tenía que haber información adicional sobre cómo hacer realmente el cortocircuito (es decir, calcular el resultado solo desde el primer parámetro). El resultado tendría que verse más o menos así:

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);
}

Rara vez se quiere sobrecargar operator||y operator&&, porque rara vez hay un caso en el que la escritura a && bes realmente intuitiva en un contexto no booleano. Las únicas excepciones que conozco son las plantillas de expresión, por ejemplo, para DSL incrustadas. Y solo unos pocos de esos pocos casos se beneficiarían de la evaluación de cortocircuito. Las plantillas de expresión generalmente no, porque se usan para formar árboles de expresión que se evalúan más adelante, por lo que siempre necesita ambos lados de la expresión.

En resumen: ni los escritores de compiladores ni los autores de estándares sintieron la necesidad de saltar a través de aros y definir e implementar una sintaxis engorrosa adicional, solo porque uno en un millón podría tener la idea de que sería bueno tener un cortocircuito definido por el usuario operator&&y operator||, simplemente para llegar a la conclusión de que no es menos esfuerzo que escribir la lógica a mano.

Arne Mertz
fuente
¿El costo es realmente tan alto? El lenguaje de programación D permite declarar parámetros lazyque convierten la expresión dada como argumentos implícitamente en una función anónima. Esto le da a la función llamada la opción de llamar a ese argumento, o no. Entonces, si el idioma ya tiene lambdas, la sintaxis adicional necesaria es muy pequeña. "Pseudocódigo": X y (A a, perezoso B b) {if (cond (a)) {return short (a); } else {actual (a, b ()); }}
BlackJack
@BlackJack ese parámetro diferido podría implementarse aceptando un std::function<B()>, que incurriría en una cierta sobrecarga. O si está dispuesto a alinearlo, hágalo template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}. Y tal vez sobrecargarlo con el Bparámetro "normal" , para que la persona que llama pueda decidir qué versión elegir. La lazysintaxis puede ser más conveniente pero tiene una cierta compensación de rendimiento.
Arne Mertz
1
Uno de los problemas con std::functionversus lazyes que el primero se puede evaluar varias veces. Un parámetro diferido fooque se usa como foo+footodavía solo se evalúa una vez.
MSalters
"el uso de cortocircuitos se restringiría a los casos en que Y sea convertible a X" ... no, se limita a los casos en los que Xse puede calcular Ysolo. Muy diferente. std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}. A menos que esté utilizando un uso muy informal de "conversión".
Mooing Duck
1
@Sumant pueden. Pero también puede escribir a mano la lógica de una costumbre de cortocircuito operator&&. La pregunta no es si es posible, sino por qué no hay una manera conveniente y corta.
Arne Mertz
5

Lambdas no es la única forma de introducir la pereza. La evaluación diferida es relativamente sencilla utilizando plantillas de expresión en C ++. No hay necesidad de palabras clave lazyy se puede implementar en C ++ 98. Los árboles de expresión ya se mencionan anteriormente. Las plantillas de expresión son árboles de expresión del hombre pobre (pero inteligente). El truco consiste en convertir la expresión en un árbol de instancias recursivamente anidadas de la Exprplantilla. El árbol se evalúa por separado después de la construcción.

Los siguientes implementos de código corto-circuito &&y ||operadores para la clase Ssiempre que proporciona logical_andy logical_orfunciones libres y que se puede convertir en bool. El código está en C ++ 14 pero la idea también es aplicable en C ++ 98. Ver ejemplo en vivo .

#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
fuente
5

Se permite el cortocircuito de los operadores lógicos porque es una "optimización" en la evaluación de las tablas de verdad asociadas. Es una función de la lógica misma, y ​​esta lógica está definida.

¿Existe realmente una razón por la que se sobrecarga &&y ||no se cortocircuita?

Los operadores lógicos sobrecargados personalizados no están obligados a seguir la lógica de estas tablas de verdad.

Pero, ¿por qué pierden este comportamiento cuando se sobrecargan?

Por lo tanto, toda la función debe evaluarse según lo normal. El compilador debe tratarlo como un operador (o función) sobrecargado normal y aún puede aplicar optimizaciones como lo haría con cualquier otra función.

La gente sobrecarga los operadores lógicos por una variedad de razones. Por ejemplo; pueden tener un significado específico en un dominio específico que no es el lógico "normal" al que la gente está acostumbrada.

Niall
fuente
4

El cortocircuito se debe a la tabla de verdad de "y" y "o". ¿Cómo sabría qué operación va a definir el usuario y cómo sabe que no tendrá que evaluar al segundo operador?

nj-ath
fuente
Como se menciona en los comentarios y en la respuesta de @Deduplicators, sería posible con una función de idioma adicional. Sé que no funciona ahora. Mi pregunta fue cuál es el razonamiento detrás de que no exista tal característica.
iFreilicht
Bueno, sin duda sería una característica complicada, ¡teniendo en cuenta que tenemos que aventurarnos a adivinar la definición que el usuario tiene de ella!
nj-ath
¿Qué pasa : (<condition>)después de la declaración del operador para especificar una condición en la que no se evalúa el segundo argumento?
iFreilicht
@iFreilicht: Todavía necesitaría un cuerpo alternativo de función unaria.
MSalters
3

pero los operadores de bool tienen este comportamiento, ¿por qué debería restringirse a este tipo único?

Solo quiero responder esta parte. La razón es que el incorporado &&y las ||expresiones no se implementan con funciones como lo están los operadores sobrecargados.

Tener la lógica de cortocircuito integrada en la comprensión del compilador de expresiones específicas es fácil. Es como cualquier otro flujo de control incorporado.

Pero la sobrecarga del operador se implementa con funciones en su lugar, que tienen reglas particulares, una de las cuales es que todas las expresiones utilizadas como argumentos se evalúan antes de llamar a la función. Obviamente, se podrían definir reglas diferentes, pero ese es un trabajo más grande.

bames53
fuente
1
Me pregunto si se le dio ninguna consideración a la cuestión de si las sobrecargas de &&, ||y ,se debe permitir? El hecho de que C ++ no tenga un mecanismo para permitir que las sobrecargas se comporten como algo diferente a las llamadas a funciones explica por qué las sobrecargas de esas funciones no pueden hacer otra cosa, pero no explica por qué esos operadores son sobrecargables en primer lugar. Sospecho que la verdadera razón es simplemente que fueron incluidos en una lista de operadores sin pensarlo mucho.
supercat