Existe realmente uma razão pela qual sobrecarregado && e || não curto-circuito?

137

O comportamento em curto-circuito dos operadores &&e ||é uma ferramenta incrível para programadores.

Mas por que eles perdem esse comportamento quando sobrecarregados? Entendo que os operadores são apenas açúcar sintático para funções, mas os operadores booltêm esse comportamento. Por que deveria ser restrito a esse tipo único? Existe algum raciocínio técnico por trás disso?

iFreilicht
fonte
1
@PiotrS. Essa pergunta é provavelmente a resposta. Eu acho que o padrão poderia definir uma nova sintaxe apenas para esse propósito. Provavelmente como operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht 18/09/14
1
@PiotrS .: Considere lógica tri-state: {true, false, nil}. Uma vez nil&& x == nilque poderia causar um curto-circuito.
MSalters
1
@ MSalters: Considere std::valarray<bool> a, b, c;, como você imagina a || b || cestar em curto-circuito?
Piotr Skotnicki
4
@PiotrS .: Estou argumentando que existe pelo menos um tipo não booleano para o qual curto-circuito faz sentido. Não estou argumentando que o curto-circuito faz sentido para todo tipo não-booleano.
MSalters
3
Ninguém mencionou isso ainda, mas também há a questão da compatibilidade com versões anteriores. A menos que um cuidado especial seja dedicado à limitação das circunstâncias em que esse curto-circuito se aplicaria, esse curto-circuito poderia quebrar o código existente que sobrecarrega operator&&ou operator||depende dos dois operandos avaliados. Manter a compatibilidade com versões anteriores é (ou deveria ser) importante ao adicionar recursos a um idioma existente.
David Hammen

Respostas:

151

Todos os processos de design resultam em compromissos entre objetivos mutuamente incompatíveis. Infelizmente, o processo de design do &&operador sobrecarregado em C ++ produziu um resultado final confuso: que o próprio recurso que você deseja &&- seu comportamento em curto-circuito - é omitido.

Os detalhes de como esse processo de design terminou neste local infeliz, aqueles que eu não conheço. No entanto, é relevante ver como um processo de design posterior levou esse resultado desagradável em consideração. Em C #, o &&operador sobrecarregado está em curto-circuito. Como os designers de C # conseguiram isso?

Uma das outras respostas sugere "levantamento lambda". Isso é:

A && B

poderia ser percebido como algo moralmente equivalente a:

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

onde o segundo argumento usa algum mecanismo para avaliação lenta, para que, quando avaliados, sejam produzidos os efeitos colaterais e o valor da expressão. A implementação do operador sobrecarregado só faria a avaliação preguiçosa quando necessário.

Não foi o que a equipe de design do C # fez. (Além disso: embora o levantamento lambda seja o que eu fiz quando chegou a hora de fazer a representação em árvore de expressão do ??operador, o que exige que certas operações de conversão sejam executadas com preguiça. Descrever isso em detalhes seria, no entanto, uma digressão importante. Basta dizer: levantamento lambda funciona, mas é suficientemente pesado que desejávamos evitá-lo.)

Em vez disso, a solução C # divide o problema em dois problemas separados:

  • devemos avaliar o operando do lado direito?
  • se a resposta acima foi "sim", como combinamos os dois operandos?

Portanto, o problema é resolvido, tornando ilegal a sobrecarga &&direta. Em vez disso, em C # você deve sobrecarregar dois operadores, cada um dos quais responde a uma dessas duas perguntas.

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

(Além disso: na verdade, três. O C # exige que, se o operador falsefor fornecido, o operador truetambém deve ser fornecido, o que responde à pergunta: isso é "verdadeiro?". Normalmente, não haveria razão para fornecer apenas um operador para C # requer ambos.)

Considere uma declaração do formulário:

C cresult = cleft && cright;

O compilador gera código para isso como se você tivesse escrito este pseudo-C #:

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

Como você pode ver, o lado esquerdo é sempre avaliado. Se for determinado como "falso-ish", será o resultado. Caso contrário, o lado direito é avaliado, e o ansioso operador definido pelo utilizador &é invocada.

O ||operador é definido da maneira análoga, como uma invocação do operador true e do |operador ansioso :

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

Ao definir todas as quatro operadoras - true, false, &e |- C # permite-lhe não só dizer cleft && cright, mas também não curto-circuito cleft & cright, e também if (cleft) if (cright) ..., e c ? consequence : alternativee while(c), e assim por diante.

Agora, eu disse que todos os processos de design são o resultado de um compromisso. Aqui, os designers de linguagem C # conseguiram um curto-circuito &&e a correção ||, mas isso exige sobrecarregar quatro operadores em vez de dois , o que algumas pessoas acham confuso. O recurso verdadeiro / falso do operador é um dos recursos menos compreendidos em C #. O objetivo de ter uma linguagem sensível e direta, familiar aos usuários de C ++, era contrariado pelo desejo de ter um curto-circuito e pelo desejo de não implementar o levantamento lambda ou outras formas de avaliação lenta. Penso que era uma posição de compromisso razoável, mas é importante perceber que é uma posição de compromisso. Apenas um diferente posição de compromisso do que os projetistas de C ++ chegaram.

Se o assunto do design de linguagem para esses operadores lhe interessar, considere ler minha série sobre por que o C # não define esses operadores em booleanos anuláveis:

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

Eric Lippert
fonte
1
@Deduplicator: Você pode também estar interessado em ler esta pergunta e responde: stackoverflow.com/questions/5965968/...
Eric Lippert
5
Nesse caso, acho que o compromisso é mais do que justificado. A coisa complicada é algo com o qual apenas o arquiteto de uma biblioteca de classes deve se preocupar e, em troca dessa complicação, torna o consumo da biblioteca mais fácil e intuitivo.
Cody Gray
1
@EricLippert Eu acredito que a Envision estava afirmando que ele viu este post e pensou que era você ... então viu que ele estava certo. Ele não estava dizendo que your posté irrelevante. His noticing your distinct writing styleé irrelevante.
WernerCD 19/09/14
5
A equipe da Microsoft não recebe crédito suficiente por (1) fazer um bom esforço para fazer a coisa certa em C # e (2) fazer a coisa certa mais vezes do que não.
Codenheim
2
@Voo: Se você optar por implementar uma conversão implícita para bool, poderá usar &&e ||sem implementar operator true/falseou operator &/|em C # não há problema. O problema surge precisamente na situação em que não há conversão boolpossível , ou onde não é desejado.
Eric Lippert 26/09
43

O ponto é que (dentro dos limites do C ++ 98) o operando do lado direito seria passado para o operador sobrecarregado como argumento. Ao fazer isso, ele já seria avaliado . Não há nada que o código operator||()ou operator&&()possa ou não faça que evite isso.

O operador original é diferente, porque não é uma função, mas implementado em um nível inferior do idioma.

Recursos de linguagem adicionais poderiam ter feito não-avaliação do direito operando sintaticamente possível . No entanto, eles não se incomodaram porque há apenas alguns casos selecionados em que isso seria semanticamente útil. (Assim como ? :, que não está disponível para sobrecarga.

(Eles levaram 16 anos para colocar lambdas no padrão ...)

Quanto ao uso semântico, considere:

objectA && objectB

Isso se resume a:

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

Pense no que exatamente você gostaria de fazer com o objeto B (de tipo desconhecido) aqui, além de chamar um operador de conversão boole como você colocaria isso em palavras para a definição de idioma.

E se você está chamando conversão para bool, bem ...

objectA && obectB

faz a mesma coisa, agora faz? Então, por que sobrecarregar em primeiro lugar?

DevSolar
fonte
7
bem, seu erro lógico é raciocinar na linguagem atualmente definida sobre os efeitos de uma linguagem definida de maneira diferente. antigamente, muitos novatos costumavam fazer isso. "construtor virtual". foi necessária uma quantidade excessiva de explicações para tirá-las desse pensamento de caixa. de qualquer maneira, com o curto-circuito dos operadores internos, há garantias sobre a não avaliação de argumentos. essa garantia também estaria presente para sobrecargas definidas pelo usuário, se um curto-circuito fosse definido para elas.
Saúde e hth. - Alf
1
@iFreilicht: Eu basicamente disse a mesma coisa que Deduplicator ou Piotr, apenas com palavras diferentes. Eu elaborei um pouco sobre o ponto na resposta editada. Era muito mais conveniente dessa maneira, as extensões de idioma necessárias (por exemplo, lambdas) não existiam até recentemente, e o benefício teria sido insignificante de qualquer maneira. As poucas vezes em que as pessoas responsáveis ​​teriam "gostado" de algo que ainda não havia sido feito pelos construtores de compiladores, em 1998, deram errado. (Veja export.)
DevSolar
9
@iFreilicht: Um booloperador de conversão para qualquer classe também tem acesso a todas as variáveis ​​de membro e funciona bem com o operador interno. Qualquer outra coisa que não seja conversão para bool não faz sentido semântico para avaliação de curto-circuito de qualquer maneira! Tente abordar isso do ponto de vista semântico, não sintático: o que você tentaria alcançar, não como o faria.
precisa saber é o seguinte
1
Eu tenho que admitir que não consigo pensar em um. A única razão pela qual o curto-circuito existe é porque economiza tempo para operações em booleanos e você pode saber o resultado de uma expressão antes de todos os argumentos serem avaliados. Com outros e operações, que não é o caso, e é por isso &e &&não são o mesmo operador. Obrigado por me ajudar a perceber isso.
iFreilicht
8
@iFreilicht: O objetivo do curto-circuito é que o cálculo do lado esquerdo pode estabelecer a verdade de uma pré-condição do lado direito . if (x != NULL && x->foo)requer curto-circuito, não para velocidade, mas para segurança.
Eric Lippert
26

Um recurso deve ser pensado, projetado, implementado, documentado e enviado.

Agora, pensamos nisso, vamos ver por que agora pode ser fácil (e difícil de fazer). Lembre-se também de que há apenas uma quantidade limitada de recursos; portanto, adicioná-lo pode ter cortado outra coisa (o que você gostaria de renunciar a isso?).


Em teoria, todos os operadores poderiam permitir um comportamento em curto-circuito com apenas um recurso de linguagem adicional "menor" , a partir do C ++ 11 (quando as lambdas foram introduzidas, 32 anos após o início do "C com classes" em 1979, ainda era respeitável). após c ++ 98):

O C ++ precisaria apenas de uma maneira de anotar um argumento como avaliado preguiçosamente - um lambda oculto - para evitar a avaliação até que necessário e permitido (condições prévias atendidas).


Como seria esse recurso teórico (lembre-se de que qualquer novo recurso deve ser amplamente utilizável)?

Uma anotação lazy, aplicada a um argumento de função, torna a função um modelo esperando um functor e faz com que o compilador empacote a expressão em um functor:

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

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

Ficaria embaixo da capa 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;});

Observe que o lambda permanece oculto e será chamado no máximo uma vez.
Não deve haver degradação no desempenho devido a isso, além das chances reduzidas de eliminação de subexpressão comum.


Além da complexidade da implementação e da complexidade conceitual (cada recurso aumenta os dois, a menos que facilite suficientemente essas complexidades para outros recursos), vejamos outra consideração importante: Compatibilidade com versões anteriores.

Embora esse recurso de idioma não quebre nenhum código, ele alterará sutilmente qualquer API aproveitando-o, o que significa que qualquer uso nas bibliotecas existentes seria uma alteração de interrupção silenciosa.

BTW: Esse recurso, embora mais fácil de usar, é estritamente mais forte que a solução C # de divisão &&e ||em duas funções para definição separada.

Desduplicador
fonte
6
@iFreilicht: Alguma pergunta no formato "por que o recurso X não existe?" tem a mesma resposta: para existir, o recurso deve ter sido pensado, considerado uma boa idéia, projetado, especificado, implementado, testado, documentado e enviado ao usuário final. Se alguma dessas coisas não aconteceu, não há recurso. Uma dessas coisas não aconteceu com o recurso proposto; descobrir qual é um problema histórico de pesquisa; comece a conversar com as pessoas no comitê de design se você se importa com qual dessas coisas nunca foi feita.
Eric Lippert
1
@ Ericricippert: E, dependendo de qual motivo, repita até que seja implementado: talvez tenha sido considerado muito complicado e ninguém tenha pensado em fazer uma reavaliação. Ou a reavaliação terminou com diferentes motivos para rejeitar do que o realizado anteriormente. (btw: Adicionado a essência do seu comentário)
Deduplicator
@Duplicador Com modelos de expressão, nem a palavra-chave lenta nem as lambdas são necessárias.
Sumant
Como um aparte histórico, observe que a linguagem original do Algol 68 teve uma coerção "procedimental" (assim como uma deprocedura, o que significa chamar implicitamente uma função sem parâmetros quando o contexto requer o tipo de resultado e não o tipo de função). Isso significa que uma expressão do tipo T em uma posição que requer um valor do tipo "função sem parâmetros retornando T" (escrita " proc T" no Algol 68) seria implicitamente transformada em corpo da função retornando a expressão fornecida (lambda implícita). O recurso foi removido (ao contrário da desclassificação) na revisão de 1973 do idioma.
Marc van Leeuwen
... Para C ++, uma abordagem semelhante poderia ser declarar que os operadores &&adotam um argumento do tipo "ponteiro para retornar T" e uma regra de conversão adicional que permite que uma expressão de argumento do tipo T seja convertida implicitamente em uma expressão lambda. Observe que essa não é uma conversão comum, pois deve ser feita no nível sintático: transformar em tempo de execução um valor do tipo T em uma função seria inútil, pois a avaliação já teria sido feita.
Marc van Leeuwen
13

Com racionalização retrospectiva, principalmente porque

  • Para garantir um curto-circuito garantido (sem a introdução de nova sintaxe), os operadores teriam que se restringir a resultadosprimeiro argumento real conversível em boole

  • curto-circuito pode ser facilmente expresso de outras maneiras, quando necessário.


Por exemplo, se uma classe Ttiver associados &&e ||operadores, a expressão

auto x = a && b || c;

onde a, be csão expressões do tipo Tpode ser expressa com curto-circuito como

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

ou talvez mais 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; }
}();

A aparente redundância preserva qualquer efeito colateral das invocações do operador.


Enquanto a reescrita lambda é mais detalhada, seu melhor encapsulamento permite definir esses operadores.

Não tenho muita certeza da conformidade padrão de todos os itens a seguir (ainda um pouco influentes), mas ele é compilado corretamente com o Visual C ++ 12.0 (2013) e o 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;
            }
        }
    }
}

Resultado:

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

Aqui cada !!bang-bang mostra uma conversão para bool, ou seja, uma verificação do valor do argumento.

Como um compilador pode facilmente fazer o mesmo e otimizá-lo adicionalmente, essa é uma implementação possível demonstrada e qualquer reivindicação de impossibilidade deve ser colocada na mesma categoria que as reivindicações de impossibilidade em geral, ou seja, geralmente besteiras.

Felicidades e hth. - Alf
fonte
Gosto das suas substituições de curto-circuito, especialmente a ternária, que é a mais próxima possível.
iFreilicht
Você está sentindo falta do curto-circuito do &&- seria necessário haver uma linha adicional como if (!a) { return some_false_ish_T(); }- e para o seu primeiro marcador: o curto-circuito é sobre os parâmetros conversíveis em bool, não os resultados.
Arne Mertz
@ArneMertz: seu comentário sobre "Missing" é aparentemente sem sentido. o comentário sobre o que é, sim, eu estou ciente disso. conversão para boolé necessário para fazer um curto-circuito.
Saúde e hth. - Alf
@ Cheersandhth.-Alf, o comentário sobre falta foi para a primeira revisão de sua resposta, onde você fez um curto-circuito no ||mas não no &&. O outro comentário foi direcionado para "teria que ser restrito a resultados conversíveis em bool" no seu primeiro marcador - ele deveria ler "restrito a parâmetros conversíveis em bool" imo.
Arne Mertz
@ArneMertz: OK, re versão, desculpe, eu estou editando lentamente. Restringido, não, é o resultado do operador que deve ser restringido, porque ele deve ser convertido para bool, a fim de verificar se há um curto-circuito de outros operadores na expressão. Assim, o resultado de a && bdeve ser convertido em boolpara verificar se há um curto-circuito no OR lógico em a && b || c.
Saúde e hth. - Alf
5

tl; dr : não vale a pena o esforço, devido à demanda muito baixa (quem usaria o recurso?) em comparação com custos bastante altos (sintaxe especial necessária).

A primeira coisa que vem à mente é que a sobrecarga do operador é apenas uma maneira elegante de escrever funções, enquanto a versão booleana dos operadores ||e &&as coisas são comuns. Isso significa que o compilador tem a liberdade de colocá-lo em curto-circuito, enquanto a expressão é não x = y && z-booleana ye zdeve levar a uma chamada para uma função como X operator&& (Y, Z). Isso significa que y && zé apenas uma maneira elegante de escrever, operator&&(y,z)que é apenas uma chamada de uma função com nome estranho, onde ambos os parâmetros devem ser avaliados antes de chamar a função (incluindo qualquer coisa que considere um apropriado em curto-circuito).

No entanto, alguém poderia argumentar que deveria ser possível tornar a tradução de &&operadores um pouco mais sofisticada, como é para o newoperador que é traduzido em chamar a função operator newseguida por uma chamada de construtor.

Tecnicamente, isso não seria um problema, seria necessário definir uma sintaxe de linguagem específica para a pré-condição que permita um curto-circuito. No entanto, o uso de curtos-circuitos seria restrito aos casos em que Yé convetível X, ou então havia informações adicionais sobre como realmente fazer o curto-circuito (ou seja, calcular o resultado apenas do primeiro parâmetro). O resultado teria que ser algo como isto:

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

Raramente se deseja sobrecarregar operator||e operator&&, porque raramente existe um caso em que a escrita a && bé realmente intuitiva em um contexto não-booleano. As únicas exceções que conheço são modelos de expressão, por exemplo, para DSLs incorporadas. E apenas alguns desses poucos casos se beneficiariam da avaliação de curto-circuito. Os modelos de expressão geralmente não funcionam, porque são usados ​​para formar árvores de expressão que são avaliadas posteriormente, portanto, você sempre precisa dos dois lados da expressão.

Resumindo: nem os escritores de compiladores nem os autores de padrões sentiram a necessidade de passar por obstáculos e definir e implementar uma sintaxe complicada adicional, apenas porque um em um milhão pode ter a idéia de que seria bom ter um curto-circuito no definido pelo usuário operator&&e operator||- apenas para chegar à conclusão de que não é menos esforço do que escrever a lógica manualmente.

Arne Mertz
fonte
O custo é realmente tão alto? A linguagem de programação D permite declarar parâmetros, lazytransformando a expressão dada como argumento implicitamente em uma função anônima. Isso dá à função chamada a opção de chamar esse argumento, ou não. Portanto, se o idioma já possui lambdas, a sintaxe extra necessária é muito pequena. “Pseudocódigo”: X e (A a, preguiçoso B b) {if (cond (a)) {retornam curto (a); } else {atual (a, b ()); }}
BlackJack
@BlackJack esse parâmetro lento poderia ser implementado aceitando a std::function<B()>, o que geraria uma certa sobrecarga. Ou, se você estiver disposto a incorporar, faça isso template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}. E talvez sobrecarregá-lo com o Bparâmetro "normal" , para que o chamador possa decidir qual versão escolher. A lazysintaxe pode ser mais conveniente, mas possui uma certa relação de desempenho.
Arne Mertz
1
Um dos problemas com std::functionversus lazyé que o primeiro pode ser avaliado várias vezes. Um parâmetro lento fooque é usado como foo+fooainda é avaliado apenas uma vez.
precisa saber é o seguinte
"o uso de curtos-circuitos seria restrito aos casos em que Y é convetível para X" ... não, é restrito aos casos em que Xpode ser calculado Yapenas. Muito diferente. std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}. A menos que você esteja usando um uso muito casual de "conversão".
Mooing Duck
1
@Sumant eles podem. Mas você também pode escrever à mão a lógica de um costume de curto-circuito operator&&. A questão não é se é possível, mas por que não existe uma maneira curta e conveniente.
Arne Mertz
5

Lambdas não é a única maneira de introduzir a preguiça. A avaliação preguiçosa é relativamente direta usando modelos de expressão em C ++. Não há necessidade de palavra lazy- chave e ela pode ser implementada no C ++ 98. Árvores de expressão já são mencionadas acima. Modelos de expressão são árvores de expressão do homem pobre (mas inteligente). O truque é converter a expressão em uma árvore de instanciações recursivamente aninhadas do Exprmodelo. A árvore é avaliada separadamente após a construção.

O código a seguir implementa curto-circuito &&e ||operadores para a classe S, desde que forneça logical_ande logical_orlibere funções e seja convertível em bool. O código está em C ++ 14, mas a ideia também é aplicável em C ++ 98. Veja exemplo ao 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
fonte
5

É permitido um curto-circuito nos operadores lógicos porque é uma "otimização" na avaliação das tabelas verdadeiras associadas. É uma função da própria lógica , e essa lógica é definida.

Existe realmente uma razão para sobrecarregar &&e ||não causar um curto-circuito?

Os operadores lógicos sobrecarregados personalizados não são obrigados a seguir a lógica dessas tabelas verdadeiras.

Mas por que eles perdem esse comportamento quando sobrecarregados?

Portanto, toda a função precisa ser avaliada como normalmente. O compilador deve tratá-lo como um operador sobrecarregado normal (ou função) e ainda pode aplicar otimizações como faria com qualquer outra função.

As pessoas sobrecarregam os operadores lógicos por vários motivos. Por exemplo; eles podem ter significado específico em um domínio específico que não é o lógico "normal" com o qual as pessoas estão acostumadas.

Niall
fonte
4

O curto-circuito é por causa da tabela de verdade de "e" e "ou". Como você saberia qual operação o usuário definirá e como você saberá que não precisará avaliar o segundo operador?

nj-ath
fonte
Conforme mencionado nos comentários e na resposta dos @Deduplicators, seria possível com um recurso de idioma adicional. Eu sei que isso não funciona agora. Minha pergunta era qual é o raciocínio por trás de não haver tal característica.
iFreilicht
Bem, certamente seria um recurso complicado, considerando que temos que arriscar um palpite sobre a definição do usuário!
Nj-ath
E : (<condition>)depois da declaração do operador para especificar uma condição na qual o segundo argumento não é avaliado?
IFreilicht 18/09/2014
@iFreilicht: Você ainda precisaria de um corpo funcional alternativo.
MSalters
3

mas os operadores para bool têm esse comportamento, por que deveria ser restrito a esse tipo único?

Eu só quero responder esta parte. O motivo é que o built-in &&e as ||expressões não são implementadas com funções como os operadores sobrecarregados.

É fácil ter a lógica de curto-circuito incorporada ao entendimento do compilador sobre expressões específicas. É como qualquer outro fluxo de controle interno.

Mas a sobrecarga do operador é implementada com funções, que possuem regras específicas, uma das quais é que todas as expressões usadas como argumentos são avaliadas antes que a função seja chamada. Obviamente, regras diferentes podem ser definidas, mas esse é um trabalho maior.

bames53
fonte
1
Pergunto-me se qualquer consideração foi dada à questão de se sobrecargas &&, ||e ,deve ser permitida? O fato de o C ++ não ter um mecanismo para permitir que as sobrecargas se comportem como algo além de chamadas de função explica por que as sobrecargas dessas funções não podem fazer mais nada, mas não explica por que esses operadores são sobrecarregáveis ​​em primeiro lugar. Eu suspeito que o verdadeiro motivo é simplesmente porque eles foram lançados em uma lista de operadores sem muita reflexão.
Supercat