== e! = São mutuamente dependentes?

292

Estou aprendendo sobre a sobrecarga de operadores em C ++ e vejo isso ==e !=são simplesmente algumas funções especiais que podem ser personalizadas para tipos definidos pelo usuário. Minha preocupação é, porém, por que são necessárias duas definições separadas ? Eu pensei que se a == bé verdade, então a != bé automaticamente falsa e vice-versa, e não há outra possibilidade, porque, por definição, a != bé !(a == b). E eu não conseguia imaginar nenhuma situação em que isso não fosse verdade. Mas talvez minha imaginação seja limitada ou eu ignore alguma coisa?

Eu sei que posso definir um em termos do outro, mas não é sobre isso que estou perguntando. Também não estou perguntando sobre a distinção entre comparar objetos por valor ou por identidade. Ou se dois objetos podem ser iguais e não iguais ao mesmo tempo (isso definitivamente não é uma opção! Essas coisas são mutuamente exclusivas). O que estou perguntando é o seguinte:

Existe alguma situação possível em que fazer perguntas sobre dois objetos serem iguais faz sentido, mas perguntar sobre eles não serem iguais não faz sentido? (da perspectiva do usuário ou da perspectiva do implementador)

Se não existe essa possibilidade, por que C ++ na Terra esses dois operadores são definidos como duas funções distintas?

BarbaraKwarc
fonte
13
Dois ponteiros podem ser nulos, mas não necessariamente iguais.
Ali Caglayan
2
Não tenho certeza se faz sentido aqui, mas ler isso me fez pensar em problemas de 'curto-circuito'. Por exemplo, pode-se definir que 'undefined' != expressioné sempre verdadeiro (ou falso ou indefinido), independentemente de a expressão poder ser avaliada. Nesse caso a!=b, retornaria o resultado correto conforme a definição, mas !(a==b)falharia se bnão puder ser avaliado. (Ou gaste muito tempo se a avaliação bfor cara).
Dennis Jaheruddin 15/06
2
E quanto a null! = Null e null == null? Pode ser ambos ... então, se a! = B nem sempre significa a == b.
zozo
4
Um exemplo de javascript(NaN != NaN) == true
chiliNUT

Respostas:

272

Você poderia não deseja que o idioma para reescrever automaticamente a != bcomo !(a == b)quando a == bretorna algo diferente de um bool. E há algumas razões pelas quais você pode fazer isso.

Você pode ter objetos do construtor de expressões, nos quais a == bnão tem e não se destina a realizar nenhuma comparação, mas simplesmente cria algum nó de expressão representando a == b.

Você pode ter uma avaliação preguiçosa, onde a == bnão pretende e não realiza nenhuma comparação diretamente, mas retorna algum tipo lazy<bool>que pode ser convertido em boolimplicitamente ou explicitamente em algum momento posterior para realmente executar a comparação. Possivelmente combinado com os objetos do construtor de expressões para permitir otimização completa da expressão antes da avaliação.

Você pode ter alguma optional<T>classe de modelo personalizada , onde são fornecidas variáveis ​​opcionais te u, você deseja permitir t == u, mas faça com que ela retorne optional<bool>.

Provavelmente há mais em que não pensei. E mesmo que nesses exemplos a operação a == be a != bas duas façam sentido, ainda a != bnão é a mesma coisa !(a == b), portanto são necessárias definições separadas.


fonte
72
A construção de expressões é um exemplo prático fantástico de quando você deseja isso, que não depende de cenários planejados.
Oliver Charlesworth
6
Outro bom exemplo seria operações lógicas de vetor. Você prefere uma passagem pelos dados de computação !=em vez de duas passagens de computação ==em seguida !. Especialmente no dia em que você não podia confiar no compilador para fundir os loops. Ou ainda hoje, se você não conseguir convencer o compilador, seus vetores não se sobrepõem.
41
"Você pode ter objetos do construtor de expressões" - bem, o operador !também pode construir algum nó de expressão e ainda estamos substituindo a != b-o !(a == b)por enquanto. O mesmo vale para lazy<bool>::operator!, pode retornar lazy<bool>. optional<bool>é mais convincente, pois a veracidade lógica de, por exemplo, boost::optionaldepende da existência de um valor, não do valor em si.
21716 Steve Joplin
42
Tudo isso Nanes - por favor lembre-se dos NaN;
Jsbueno
9
@jsbueno: foi apontado mais adiante que os NaNs não são especiais nesse sentido.
Oliver Charlesworth
110

Se não existe essa possibilidade, por que C ++ na Terra esses dois operadores são definidos como duas funções distintas?

Porque você pode sobrecarregá-los e, sobrecarregando-os, pode dar a eles um significado totalmente diferente do original.

Tomemos, por exemplo, operador <<, originalmente o operador de mudança à esquerda bit a bit, agora geralmente sobrecarregado como um operador de inserção, como em std::cout << something; significado totalmente diferente do original.

Portanto, se você aceitar que o significado de um operador muda quando você o sobrecarrega, não há motivo para impedir que o usuário dê um significado ao operador ==que não seja exatamente a negação do operador !=, embora isso possa ser confuso.

picanço
fonte
18
Esta é a única resposta que faz sentido prático.
Sonic Atom
2
Para mim, parece que você tem a causa e o efeito ao contrário. Você pode sobrecarregá-los separadamente, pois ==e !=existir como operadoras distintas. Por outro lado, eles provavelmente não existem como operadores distintos porque você pode sobrecarregá-los separadamente, mas devido a motivos herdados e de conveniência (brevidade do código).
Nitro2k01 18/06/2016
60

Minha preocupação é, porém, por que são necessárias duas definições separadas?

Você não precisa definir os dois.
Se eles são mutuamente exclusivos, você ainda pode ser conciso definindo apenas ==e <ao lado de std :: rel_ops

De preferência:

#include <iostream>
#include <utility>

struct Foo {
    int n;
};

bool operator==(const Foo& lhs, const Foo& rhs)
{
    return lhs.n == rhs.n;
}

bool operator<(const Foo& lhs, const Foo& rhs)
{
    return lhs.n < rhs.n;
}

int main()
{
    Foo f1 = {1};
    Foo f2 = {2};
    using namespace std::rel_ops;

    //all work as you would expect
    std::cout << "not equal:     : " << (f1 != f2) << '\n';
    std::cout << "greater:       : " << (f1 > f2) << '\n';
    std::cout << "less equal:    : " << (f1 <= f2) << '\n';
    std::cout << "greater equal: : " << (f1 >= f2) << '\n';
}

Existe alguma situação possível em que fazer perguntas sobre dois objetos serem iguais faz sentido, mas perguntar sobre eles não serem iguais não faz sentido?

Frequentemente, associamos esses operadores à igualdade.
Embora seja assim que eles se comportam em tipos fundamentais, não há obrigação de que esse seja o comportamento deles em tipos de dados personalizados. Você nem precisa retornar um bool se não quiser.

Vi pessoas sobrecarregar os operadores de maneiras bizarras, apenas para descobrir que faz sentido para a aplicação específica de seu domínio. Mesmo que a interface pareça mostrar que eles são mutuamente exclusivos, o autor pode querer adicionar lógica interna específica.

(da perspectiva do usuário ou da perspectiva do implementador)

Eu sei que você quer um exemplo específico,
então aqui está um da estrutura de teste Catch que eu pensei que era prático:

template<typename RhsT>
ResultBuilder& operator == ( RhsT const& rhs ) {
    return captureExpression<Internal::IsEqualTo>( rhs );
}

template<typename RhsT>
ResultBuilder& operator != ( RhsT const& rhs ) {
    return captureExpression<Internal::IsNotEqualTo>( rhs );
}

Esses operadores estão fazendo coisas diferentes, e não faria sentido definir um método como um! (Não) do outro. A razão disso é que o framework pode imprimir a comparação feita. Para fazer isso, ele precisa capturar o contexto de qual operador sobrecarregado foi usado.

Trevor Hickey
fonte
14
Oh meu Deus, como eu não sabia std::rel_ops? Muito obrigado por apontar isso.
Daniel Jour
5
Cópias quase verbais da cppreference (ou de qualquer outro lugar) devem ser claramente marcadas e adequadamente atribuídas. rel_opsé horrível de qualquer maneira.
TC
@ Concordou, estou apenas dizendo que é um método que o OP pode adotar. Eu não sei como explicar rel_ops mais simples que o exemplo mostrado. Liguei para onde ele está, mas o código publicado, pois a página de referência sempre pode mudar.
Trevor Hickey
4
Você ainda precisa deixar claro que o exemplo de código é 99% da preferência cp, e não o seu.
TC
2
Std :: relops parece ter caído em desuso. Confira o boost ops para algo mais direcionado.
JDługosz
43

Existem algumas convenções muito bem estabelecidas nas quais (a == b)e ambos(a != b) são falsos, não necessariamente opostos. Em particular, no SQL, qualquer comparação com NULL produz NULL, não é verdadeiro ou falso.

Provavelmente, não é uma boa ideia criar novos exemplos disso, se possível, porque é tão pouco intuitivo, mas se você estiver tentando modelar uma convenção existente, é bom ter a opção de fazer com que seus operadores se comportem "corretamente" para isso. contexto.

Jander
fonte
4
Implementando comportamento nulo semelhante a SQL em C ++? Ewwww. Mas suponho que não seja algo que eu deva ser banido no idioma, por mais desagradável que seja.
1
@ dan1111 Mais importante, alguns tipos de SQL podem muito bem ser codificados em c ++, então a linguagem precisa suportar sua sintaxe, não?
Joe
1
Corrija-me se estiver errado, estou saindo da wikipedia aqui, mas a comparação com um valor NULL no SQL retorna Desconhecido, não Falso? E a negação do desconhecido ainda não é desconhecida? Portanto, se a lógica SQL fosse codificada em C ++, você não desejaria NULL == somethingretornar Desconhecido e também NULL != somethingretornaria Desconhecido, e desejaria !Unknownretornar Unknown. E, nesse caso, a implementação operator!=como negação de operator==ainda está correta.
Benjamin Lindley
1
@ Barmar: Ok, mas como isso faz a declaração "SQL NULLs funcionar dessa maneira" correta? Se estamos restringindo nossas implementações de operadores de comparação ao retorno de booleanos, isso não significa apenas que é impossível implementar a lógica SQL com esses operadores?
Benjamin Lindley
2
@ Barmar: Bem, não, esse não é o ponto. O OP já conhece esse fato, ou essa pergunta não existiria. O objetivo era apresentar um exemplo em que fazia sentido 1) implementar um operator==ou operator!=, mas não o outro, ou 2) implementar operator!=de uma maneira diferente da negação de operator==. E implementar a lógica SQL para valores NULL não é um caso disso.
Benjamin Lindley
23

Eu responderei apenas a segunda parte da sua pergunta, a saber:

Se não existe essa possibilidade, por que C ++ na Terra esses dois operadores são definidos como duas funções distintas?

Uma razão pela qual faz sentido permitir que o desenvolvedor sobrecarregue ambos é o desempenho. Você pode permitir otimizações implementando ambos ==e !=. Então x != ypode ser mais barato do que !(x == y)é. Alguns compiladores podem otimizar para você, mas talvez não, especialmente se você tiver objetos complexos com muitas ramificações envolvidas.

Mesmo em Haskell, onde os desenvolvedores levam leis e conceitos matemáticos muito a sério, ainda é permitido sobrecarregar ambos ==e /=, como você pode ver aqui ( http://hackage.haskell.org/package/base-4.9.0.0/docs/Prelude .html # v: -61--61- ):

$ ghci
GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
λ> :i Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
        -- Defined in `GHC.Classes'

Isso provavelmente seria considerado micro-otimização, mas pode ser justificado em alguns casos.

Centril
fonte
3
As classes de wrapper SSE (x86 SIMD) são um ótimo exemplo disso. Há uma pcmpeqbinstrução, mas nenhuma instrução de comparação compactada produzindo uma máscara! =. Portanto, se você não pode simplesmente reverter a lógica do que quer que use os resultados, precisará usar outra instrução para invertê-la. (Fato interessante: o conjunto de instruções XOP da AMD tem comparação compacta para neq. Pena que a Intel não adotou / estendeu o XOP; existem algumas instruções úteis nessa extensão ISA que está prestes a morrer.)
Peter Cordes
1
O ponto principal do SIMD em primeiro lugar é o desempenho, e você normalmente apenas se preocupa em usá-lo manualmente em loops importantes para o desempenho geral. Salvar uma única instrução ( PXORcom todas as unidades para inverter o resultado da máscara de comparação) em um loop restrito pode ser importante.
Peter Cordes
O desempenho como razão não é credível quando a sobrecarga é uma negação lógica .
Saúde e hth. # 1
Pode ser mais de uma negação lógica se a computação x == ycustar mais significativamente do que x != y. Computar o último pode ser significativamente mais barato devido à previsão de desvios, etc.
Centril
16

Existe alguma situação possível em que fazer perguntas sobre dois objetos serem iguais faz sentido, mas perguntar sobre eles não serem iguais não faz sentido? (da perspectiva do usuário ou da perspectiva do implementador)

Essa é uma opinião. Talvez não. Mas os designers de linguagem, por não serem oniscientes, decidiram não restringir as pessoas que poderiam ter situações em que isso pudesse fazer sentido (pelo menos para elas).

Benjamin Lindley
fonte
13

Em resposta à edição;

Ou seja, se é possível que algum tipo tenha o operador, ==mas não o !=, ou vice-versa, e quando faz sentido fazê-lo.

Em geral , não, não faz sentido. Operadores relacionais e de igualdade geralmente vêm em conjuntos. Se existe a igualdade, também a desigualdade; menos que, então maior que e assim por diante com o <=etc. Uma abordagem semelhante também é aplicada aos operadores aritméticos, eles geralmente também vêm em conjuntos lógicos naturais.

Isso é evidenciado no std::rel_opsespaço para nome. Se você implementar a igualdade e menos que operadores, o uso desse espaço para nome fornecerá os outros, implementados em termos de seus operadores implementados originais.

Dito isto, existem condições ou situações em que um não significaria imediatamente o outro ou não poderia ser implementado em termos dos outros? Sim, existem , sem dúvida, poucos, mas eles estão lá; novamente, como evidenciado por rel_opsser um espaço de nome próprio. Por esse motivo, permitir que eles sejam implementados de forma independente permite que você aproveite o idioma para obter a semântica necessária ou requerida de uma maneira ainda natural e intuitiva para o usuário ou cliente do código.

A avaliação preguiçosa já mencionada é um excelente exemplo disso. Outro bom exemplo é dar a eles semânticas que não significam igualdade ou desigualdade. Um exemplo semelhante a este são os operadores de troca de bits <<e >>sendo usados ​​para inserção e extração de fluxo. Embora possa ser desaprovado nos círculos gerais, em algumas áreas específicas do domínio, pode fazer sentido.

Niall
fonte
12

Se os operadores ==e !=realmente não implicam igualdade, da mesma forma que os operadores <<e >>stream não implicam mudança de bits. Se você tratar os símbolos como se eles significassem algum outro conceito, eles não precisam ser mutuamente exclusivos.

Em termos de igualdade, faria sentido se o seu caso de uso justificasse o tratamento de objetos como não comparáveis, para que toda comparação retorne falso (ou um tipo de resultado não comparável, se seus operadores retornarem não booleanos). Não consigo pensar em uma situação específica em que isso seria justificado, mas pude ver que isso é razoável o suficiente.

Taywee
fonte
7

Com grande poder, é ótimo responsável, ou pelo menos realmente bons guias de estilo.

==e !=pode ser sobrecarregado para fazer o que você quiser. É uma bênção e uma maldição. Não há garantia de que isso !=signifique !(a==b).

It'sPete
fonte
6
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

BoolPlus operator==(File& other);
BoolPlus operator!=(File& other);

Não posso justificar essa sobrecarga de operador, mas no exemplo acima é impossível definir operator!=como o "oposto" de operator==.

Dafang Cao
fonte
1
@ Snowman: Dafang não diz que é uma boa enumeração (nem uma boa idéia para definir uma enumeração como essa), é apenas um exemplo para ilustrar um ponto. Com essa definição de operador (talvez ruim), !=isso não significaria o oposto de ==.
AlainD
1
@AlainD você clicou no link que eu publiquei e conhece o propósito desse site? Isso é chamado de "humor".
1
@ Snowman: Eu certamente ... desculpe, eu perdi era um link e pretendia ironia! : o)
AlainD 15/06
Espere, você está sobrecarregando unário ==?
LF
5

No final, o que você está verificando com esses operadores é que a expressão a == bou a != bestá retornando um valor booleano ( trueou false). Essa expressão retorna um valor booleano após a comparação, em vez de ser mutuamente exclusiva.

Anirudh Sohil
fonte
4

[..] por que são necessárias duas definições separadas?

Uma coisa a considerar é que pode haver a possibilidade de implementar um desses operadores com mais eficiência do que apenas usar a negação do outro.

(Meu exemplo aqui foi lixo, mas o ponto ainda permanece, pense nos filtros de bloom, por exemplo: Eles permitem testes rápidos se algo não estiver em um conjunto, mas testar se estiver dentro pode levar muito mais tempo.)

[..] por definição, a != bé !(a == b).

E é sua responsabilidade como programador fazer isso. Provavelmente é uma boa coisa para se escrever um teste.

Daniel Jour
fonte
4
Como !((a == rhs.a) && (b == rhs.b))não permite curto-circuito? se !(a == rhs.a), então (b == rhs.b)não será avaliado.
Benjamin Lindley
Este é um mau exemplo, no entanto. O curto-circuito não adiciona vantagem mágica aqui.
26716 Oliver Oliverworth
@ Oliver Charlesworth Sozinho, não, mas quando associado a operadores separados, ele faz: No caso de ==, ele parará de comparar assim que os primeiros elementos correspondentes não forem iguais. Mas, no caso de !=, se ele fosse implementado em termos de ==, seria necessário comparar todos os elementos correspondentes primeiro (quando todos forem iguais) para poder dizer que eles não são diferentes: P Mas quando implementado como em No exemplo acima, ele será interrompido assim que encontrar o primeiro par não igual. Grande exemplo, de fato.
BarbaraKwarc
@BenjaminLindley True, meu exemplo foi um absurdo completo. Infelizmente, não posso inventar outro caixa eletrônico, é tarde demais aqui.
Daniel Jour
1
@BarbaraKwarc: !((a == b) && (c == d))e (a != b) || (c != d)são equivalentes em termos de eficiência em curto-circuito.
Oliver Charlesworth
2

Ao personalizar o comportamento dos operadores, você pode fazê-los fazer o que quiser.

Você pode personalizar as coisas. Por exemplo, você pode personalizar uma classe. Objetos desta classe podem ser comparados apenas verificando uma propriedade específica. Sabendo que esse é o caso, você pode escrever um código específico que apenas verifique as coisas mínimas, em vez de verificar cada bit de cada propriedade no objeto inteiro.

Imagine um caso em que você possa descobrir que algo é diferente com a mesma rapidez, se não mais rápido, do que descobrir que algo é o mesmo. É verdade que, depois de descobrir se algo é igual ou diferente, você pode saber o contrário simplesmente mudando um pouco. No entanto, inverter esse bit é uma operação extra. Em alguns casos, quando o código é reexecutado muito, salvar uma operação (multiplicada por várias vezes) pode ter um aumento geral na velocidade. (Por exemplo, se você salvar uma operação por pixel de uma tela de megapixel, poderá salvar um milhão de operações. Multiplicado por 60 telas por segundo e salvar ainda mais operações.)

A resposta do hvd fornece alguns exemplos adicionais.

TOOGAM
fonte
2

Sim, porque um significa "equivalente" e outro significa "não equivalente" e esses termos são mutuamente exclusivos. Qualquer outro significado para esses operadores é confuso e deve ser evitado por todos os meios.

oliora
fonte
Eles não são mutuamente exclusivos para todos os casos. Por exemplo, dois infinitos não são iguais um ao outro e não são iguais um ao outro.
vladon
@ Vladlad pode usar um usar outro em caso genérico ? Não. Isso significa que eles simplesmente não são iguais. Tudo o resto vai para uma função especial ao invés de operador == / =!
oliora
@ Vladlad por favor, em vez de caso genérico leia todos os casos na minha resposta.
Oliora
@ vladon Por mais que isso seja verdade em matemática, você pode dar um exemplo em que a != bnão é igual a !(a == b)esse motivo em C?
Nitro2k01 18/05/19
2

Talvez uma regra incomparável, onde a != bera falsa e a == bera falsa como um pouco apátrida.

if( !(a == b || a != b) ){
    // Stateless
}
ToñitoG
fonte
! Se você quiser reorganizar símbolos lógicos, em seguida, ([A] || [B]) logicamente torna-se ([A!] E [B!])
Thijser
Note-se que o tipo de retorno operator==()e operator!=()não são necessariamente bool, eles podem ser um enum que incluem apátrida se você queria isso e ainda os operadores ainda pode ser definido de modo (a != b) == !(a==b)detém ..
Lorro