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 bool
têm esse comportamento. Por que deveria ser restrito a esse tipo único? Existe algum raciocínio técnico por trás disso?
operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
{true, false, nil}
. Uma veznil&& x == nil
que poderia causar um curto-circuito.std::valarray<bool> a, b, c;
, como você imaginaa || b || c
estar em curto-circuito?operator&&
ouoperator||
depende dos dois operandos avaliados. Manter a compatibilidade com versões anteriores é (ou deveria ser) importante ao adicionar recursos a um idioma existente.Respostas:
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 é:
poderia ser percebido como algo moralmente equivalente a:
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:
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.(Além disso: na verdade, três. O C # exige que, se o operador
false
for fornecido, o operadortrue
també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:
O compilador gera código para isso como se você tivesse escrito este pseudo-C #:
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 :Ao definir todas as quatro operadoras -
true
,false
,&
e|
- C # permite-lhe não só dizercleft && cright
, mas também não curto-circuitocleft & cright
, e tambémif (cleft) if (cright) ...
, ec ? consequence : alternative
ewhile(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/
fonte
your post
é irrelevante.His noticing your distinct writing style
é irrelevante.bool
, poderá usar&&
e||
sem implementaroperator true/false
ouoperator &/|
em C # não há problema. O problema surge precisamente na situação em que não há conversãobool
possível , ou onde não é desejado.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||()
ouoperator&&()
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:
Isso se resume a:
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
bool
e como você colocaria isso em palavras para a definição de idioma.E se você está chamando conversão para bool, bem ...
faz a mesma coisa, agora faz? Então, por que sobrecarregar em primeiro lugar?
fonte
export
.)bool
operador 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.&
e&&
não são o mesmo operador. Obrigado por me ajudar a perceber isso.if (x != NULL && x->foo)
requer curto-circuito, não para velocidade, mas para segurança.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:Ficaria embaixo da capa como:
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.fonte
&&
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.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 embool
ecurto-circuito pode ser facilmente expresso de outras maneiras, quando necessário.
Por exemplo, se uma classe
T
tiver associados&&
e||
operadores, a expressãoonde
a
,b
ec
são expressões do tipoT
pode ser expressa com curto-circuito comoou talvez mais claramente como
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:
Resultado:
Aqui cada
!!
bang-bang mostra uma conversão parabool
, 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.
fonte
&&
- seria necessário haver uma linha adicional comoif (!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.bool
é necessário para fazer um curto-circuito.||
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.bool
, a fim de verificar se há um curto-circuito de outros operadores na expressão. Assim, o resultado dea && b
deve ser convertido embool
para verificar se há um curto-circuito no OR lógico ema && b || c
.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ãox = y && z
-booleanay
ez
deve levar a uma chamada para uma função comoX operator&& (Y, Z)
. Isso significa quey && 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 onew
operador que é traduzido em chamar a funçãooperator new
seguida 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ívelX
, 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:Raramente se deseja sobrecarregar
operator||
eoperator&&
, porque raramente existe um caso em que a escritaa && 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&&
eoperator||
- apenas para chegar à conclusão de que não é menos esforço do que escrever a lógica manualmente.fonte
lazy
transformando 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 ()); }}std::function<B()>
, o que geraria uma certa sobrecarga. Ou, se você estiver disposto a incorporar, faça issotemplate <class F> X and(A a, F&& f){ ... actual(a,F()) ...}
. E talvez sobrecarregá-lo com oB
parâmetro "normal" , para que o chamador possa decidir qual versão escolher. Alazy
sintaxe pode ser mais conveniente, mas possui uma certa relação de desempenho.std::function
versuslazy
é que o primeiro pode ser avaliado várias vezes. Um parâmetro lentofoo
que é usado comofoo+foo
ainda é avaliado apenas uma vez.X
pode ser calculadoY
apenas. 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".operator&&
. A questão não é se é possível, mas por que não existe uma maneira curta e conveniente.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 doExpr
modelo. A árvore é avaliada separadamente após a construção.O código a seguir implementa curto-circuito
&&
e||
operadores para a classeS
, desde que forneçalogical_and
elogical_or
libere funções e seja convertível embool
. O código está em C ++ 14, mas a ideia também é aplicável em C ++ 98. Veja exemplo ao vivo .fonte
É 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.
Os operadores lógicos sobrecarregados personalizados não são obrigados a seguir a lógica dessas tabelas verdadeiras.
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.
fonte
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?
fonte
: (<condition>)
depois da declaração do operador para especificar uma condição na qual o segundo argumento não é avaliado?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.
fonte
&&
,||
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.