Por que os compiladores C ++ não definem operador == e operador! =?

302

Sou um grande fã de deixar o compilador trabalhar o máximo possível para você. Ao escrever uma classe simples, o compilador pode fornecer o seguinte de graça:

  • Um construtor padrão (vazio)
  • Um construtor de cópias
  • Um destruidor
  • Um operador de atribuição ( operator=)

Mas isso não parece fornecer operadores de comparação - como operator==ou operator!=. Por exemplo:

class foo
{
public:
    std::string str_;
    int n_;
};

foo f1;        // Works
foo f2(f1);    // Works
foo f3;
f3 = f2;       // Works

if (f3 == f2)  // Fails
{ }

if (f3 != f2)  // Fails
{ }

Existe uma boa razão para isso? Por que realizar uma comparação membro a membro seria um problema? Obviamente, se a classe alocar memória, você gostaria de ter cuidado, mas para uma classe simples, certamente o compilador poderia fazer isso por você?

Roubar
fonte
4
Obviamente, também o destruidor é fornecido gratuitamente.
Johann Gerell 20/10/08
23
Em uma de suas palestras recentes, Alex Stepanov apontou que foi um erro não ter um padrão automático ==, da mesma forma que existe uma atribuição automática padrão ( =) sob certas condições. (O argumento sobre ponteiros é inconsistente porque a lógica se aplica a =e ==, e não apenas ao segundo).
alfC 02/09/2015
2
@becko É uma das séries da A9: youtube.com/watch?v=k-meLQaYP5Y , não me lembro em qual das conversas. Há também uma proposta que parece estar chegando ao C ++ 17 open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0221r0.html
alfC 16-16:
1
@becko, é um dos primeiros da série "Programação eficiente com componentes" ou "Conversas de programação", ambas na A9, disponíveis no Youtube.
AlfC # 16/16
1
@becko Na verdade, há uma resposta abaixo apontando para o ponto de vista de Alex stackoverflow.com/a/23329089/225186
ALFC

Respostas:

71

O compilador não saberia se você queria uma comparação de ponteiro ou uma comparação profunda (interna).

É mais seguro simplesmente não implementá-lo e deixar que o programador faça isso sozinho. Então eles podem fazer todas as suposições que quiserem.

Mark Ingram
fonte
292
Esse problema não impede a geração de um copiador de cópias, onde é bastante prejudicial.
MSalters 20/10/08
78
Os construtores de cópia (e operator=) geralmente trabalham no mesmo contexto que os operadores de comparação - ou seja, há uma expectativa de que, após a execução a = b, a == bseja verdadeira. Definitivamente, faz sentido que o compilador forneça um padrão operator==usando a mesma semântica de valor agregado usada para ele operator=. Eu suspeito que paercebal esteja realmente correto aqui, pois operator=(e copie o ctor) são fornecidos apenas para compatibilidade com C, e eles não queriam piorar a situação.
Pavel Minaev 29/10/09
46
-1. Claro que você quer uma comparação profunda, se o programador queria uma comparação ponteiro, ele ia escrever (& f1 == & F2)
Viktor Sehr
62
Viktor, sugiro que você repense sua resposta. Se a classe Foo contiver um Bar *, como o compilador saberá se Foo :: operator == deseja comparar o endereço de Bar * ou o conteúdo de Bar?
Mark Ingram
46
@ Mark: Se ele contém um ponteiro, a comparação dos valores do ponteiro é razoável - se ele contém um valor, a comparação dos valores é razoável. Em circunstâncias excepcionais, o programador pode substituir. É assim como o idioma implementa a comparação entre entradas e ponteiros para entradas.
Eamon Nerbonne
317

O argumento de que, se o compilador puder fornecer um construtor de cópia padrão, ele deve ser capaz de fornecer um padrão semelhante operator==()faz certo sentido. Eu acho que o motivo da decisão de não fornecer um padrão gerado pelo compilador para esse operador pode ser adivinhado pelo que Stroustrup disse sobre o construtor de cópias padrão em "O Design e a Evolução do C ++" (Seção 11.4.1 - Controle de cópia) :

Pessoalmente, considero lamentável que as operações de cópia sejam definidas por padrão e proíbo a cópia de objetos de muitas de minhas classes. No entanto, o C ++ herdou sua atribuição padrão e construtores de cópia do C, e eles são freqüentemente usados.

Então, em vez de "por que o C ++ não possui um padrão operator==()?", A pergunta deveria ter sido "por que o C ++ tem uma atribuição padrão e um construtor de cópias?", Com a resposta sendo que esses itens foram incluídos com relutância pelo Stroustrup para compatibilidade com versões anteriores do C (provavelmente a causa da maioria das verrugas de C ++, mas também provavelmente o principal motivo da popularidade de C ++).

Para meus próprios propósitos, no meu IDE, o snippet que eu uso para novas classes contém declarações para um operador de atribuição privada e construtor de cópias, de modo que, ao gerar uma nova classe, não obtenho operações de atribuição e cópia padrão - tenho que remover explicitamente a declaração dessas operações da private:seção se eu quiser que o compilador possa gerá-las para mim.

Michael Burr
fonte
29
Boa resposta. Eu só gostaria de salientar que em C ++ 11, ao invés de fazer o operador de atribuição e construtor de cópia privada, você pode removê-los completamente como este: Foo(const Foo&) = delete; // no copy constructoreFoo& Foo=(const Foo&) = delete; // no assignment operator
karadoc
9
"No entanto, o C ++ herdou sua atribuição padrão e copia os construtores do C" Isso não implica por que você precisa criar TODOS os tipos de C ++ dessa maneira. Eles deveriam ter restringido isso apenas aos PODs antigos simples, apenas os tipos que já estão em C, não mais.
thesaint
3
Certamente eu posso entender por que o C ++ herdou esses comportamentos struct, mas desejo que ele classse comporte de maneira diferente (e sã). No processo, isso também daria uma diferença mais significativa entre structe classao lado do acesso padrão.
Jamesdlin #
@jamesdlin Se você quer uma regra, desabilitar a declaração e definição implícitas de ctors e a atribuição se um dtor for declarado faria mais sentido.
Deduplicator
1
Ainda não vejo mal algum em deixar o programador ordenar explicitamente o compilador para criar um operator==. Neste ponto, é apenas açúcar de sintaxe para algum código de placa de caldeira. Se você tem medo de que dessa maneira o programador possa ignorar algum ponteiro entre os campos de classe, você pode adicionar uma condição de que ele só funcione em tipos e objetos primitivos que possuem operadores de igualdade. Não há razão para não permitir isso inteiramente.
NO_NAME
93

Mesmo em C ++ 20, o compilador ainda não gera implicitamente operator==para você

struct foo
{
    std::string str;
    int n;
};

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ill-formed

Mas você terá a capacidade de usar o padrão explicitamente == desde C ++ 20 :

struct foo
{
    std::string str;
    int n;

    // either member form
    bool operator==(foo const&) const = default;
    // ... or friend form
    friend bool operator==(foo const&, foo const&) = default;
};

A padronização ==é feita em membro ==(da mesma maneira que o construtor de cópia padrão faz a construção de cópia em membro). As novas regras também fornecem o relacionamento esperado entre ==e !=. Por exemplo, com a declaração acima, eu posso escrever os dois:

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ok!
assert(foo{"Anton", 1} != foo{"Anton", 2}); // ok!

Esse recurso específico (padrão operator==e simetria entre ==e !=) vem de uma proposta que fazia parte do recurso de idioma mais amplo que é operator<=>.

Anton Savin
fonte
Você sabe se há alguma atualização mais recente sobre isso? Ele estará disponível no c ++ 17?
precisa
3
@ dcmm88 Infelizmente, ele não estará disponível no C ++ 17. Eu atualizei a resposta.
Anton Savin
2
A proposta modificada que permite que a mesma coisa (exceto o pequeno formulário) vai estar em C ++ 20 embora :)
Rakete1111
Então basicamente você tem que especificar = default, para algo que não é criado por padrão, certo? Parece oxímoro para mim ("padrão explícito").
Art
@artin Faz sentido, pois adicionar novos recursos ao idioma não deve prejudicar a implementação existente. Adicionar novos padrões de biblioteca ou novas coisas que o compilador pode fazer é uma coisa. Adicionar novas funções de membro onde elas não existiam anteriormente é uma história totalmente diferente. Para proteger seu projeto de erros, seria necessário muito mais esforço. Eu pessoalmente prefiro o sinalizador do compilador para alternar entre o padrão explícito e implícito. Você cria o projeto a partir do padrão C ++ mais antigo, usa o padrão explícito pelo sinalizador do compilador. Você já atualiza o compilador para configurá-lo corretamente. Para novos projetos, torne-o implícito.
Maciej Załucki 6/01
44

IMHO, não há razão "boa". A razão pela qual tantas pessoas concordam com essa decisão de design é porque elas não aprenderam a dominar o poder da semântica baseada em valor. As pessoas precisam escrever muitos construtores de cópias personalizadas, operadores de comparação e destruidores, porque usam ponteiros brutos em sua implementação.

Ao usar ponteiros inteligentes apropriados (como std :: shared_ptr), o construtor de cópia padrão geralmente é bom e a implementação óbvia do operador de comparação padrão hipotético seria muito bem.

alexk7
fonte
39

É respondido que C ++ não fez == porque C não fez, e aqui está porque C fornece apenas padrão = mas não == em primeiro lugar. C queria mantê-lo simples: C implementado = por memcpy; no entanto, == não pode ser implementado pelo memcmp devido ao preenchimento. Como o preenchimento não é inicializado, o memcmp diz que são diferentes, mesmo que sejam os mesmos. O mesmo problema existe para a classe vazia: o memcmp diz que são diferentes porque o tamanho das classes vazias não é zero. Pode-se ver acima que implementar == é mais complicado que implementar = em C. Alguns exemplos de código a respeito disso. Sua correção é apreciada se eu estiver errado.

Asa do Rio
fonte
6
O C ++ não usa o memcpy para operator=- isso funcionaria apenas para os tipos de POD, mas o C ++ também fornece um padrão operator=para os tipos não de POD.
Flexo
2
Sim, C ++ implementado = de uma maneira mais sofisticada. Parece que C acabou de ser implementado = com um simples memcpy.
Rio Wing
O conteúdo desta resposta deve ser colocado junto com o de Michael. Ele corrige a pergunta, então isso responde.
precisa saber é o seguinte
27

Neste vídeo, Alex Stepanov, o criador do STL, aborda essa questão por volta das 13:00. Para resumir, tendo observado a evolução do C ++, ele argumenta que:

  • É lamentável que == e! = Não sejam declarados implicitamente (e Bjarne concorda com ele). Uma linguagem correta deve ter essas coisas prontas para você (ele vai adiante sugerindo que você não deve ser capaz de definir um ! = Que quebra a semântica de == )
  • A razão pela qual este é o caso tem suas raízes (como muitos problemas de C ++) em C. Lá, o operador de atribuição é definido implicitamente com a atribuição bit a bit, mas isso não funcionaria para == . Uma explicação mais detalhada pode ser encontrada neste artigo na Bjarne Stroustrup.
  • Na pergunta seguinte Por que então um membro por comparação de membros não foi usado, ele diz uma coisa incrível : C era uma linguagem caseira e o cara que implementava essas coisas para Ritchie disse que achava isso difícil de implementar!

Ele então diz que no futuro (distante) == e ! = Serão implicitamente gerados.

Nikos Athanasiou
fonte
2
Parece que este futuro distante não vai ser 2017 nem 18, nem 19, bem, você me entende ...
UmNyobe
18

O C ++ 20 fornece uma maneira de implementar facilmente um operador de comparação padrão.

Exemplo de cppreference.com :

class Point {
    int x;
    int y;
public:
    auto operator<=>(const Point&) const = default;
    // ... non-comparison functions ...
};

// compiler implicitly declares operator== and all four relational operators work
Point pt1, pt2;
if (pt1 == pt2) { /*...*/ } // ok, calls implicit Point::operator==
std::set<Point> s; // ok
s.insert(pt1); // ok
if (pt1 <= pt2) { /*...*/ } // ok, makes only a single call to Point::operator<=>
vll
fonte
4
Eu estou surpreso que eles usaram Pointcomo exemplo para uma ordenação operação, uma vez que não há nenhuma maneira padrão razoável ordem dois pontos com xe ycoordenadas ...
tubo
4
@pipe Se você não se importa em qual ordem os elementos estão, usar o operador padrão faz sentido. Por exemplo, você pode usar std::setpara garantir que todos os pontos sejam exclusivos e std::setutilizem operator<apenas.
vll 24/01/19
Sobre o tipo de retorno auto: Para este caso podemos sempre supor que será std::strong_orderinga partir de #include <compare>?
kevinarpe 14/06
1
@kevinarpe O tipo de retorno é std::common_comparison_category_t, que para esta classe se torna o pedido padrão ( std::strong_ordering).
vll 15/06
15

Não é possível definir o padrão ==, mas você pode definir o padrão !=através do ==qual você normalmente deve se definir. Para isso, você deve fazer o seguinte:

#include <utility>
using namespace std::rel_ops;
...

class FooClass
{
public:
  bool operator== (const FooClass& other) const {
  // ...
  }
};

Você pode ver http://www.cplusplus.com/reference/std/utility/rel_ops/ para obter detalhes.

Além disso, se você definir operator< , os operadores para <=,>,> = poderão ser deduzidos dele ao usar std::rel_ops.

Mas você deve ter cuidado ao usar, std::rel_opsporque os operadores de comparação podem ser deduzidos para os tipos que você não espera.

A maneira mais preferida de deduzir o operador relacionado do básico é usar os operadores boost :: .

A abordagem usada no aumento é melhor porque define o uso do operador para a classe que você deseja apenas, não para todas as classes no escopo.

Você também pode gerar "+" de "+ =", - de "- =", etc ... (veja a lista completa aqui )

sergtk
fonte
Não obtive o padrão !=depois de escrever o ==operador. Ou eu fiz, mas estava faltando constness. Também tive que escrever e tudo estava bem.
John
você pode jogar com constância para alcançar os resultados necessários. Sem código, é difícil dizer o que há de errado com ele.
sergtk
2
Há uma razão rel_opspela qual foi descontinuado no C ++ 20: porque ele não funciona , pelo menos não em todos os lugares e certamente não de forma consistente. Não existe uma maneira confiável sort_decreasing()de compilar. Por outro lado, o Boost.Operators trabalha e sempre trabalhou.
Barry
10

C ++ 0x tem tido uma proposta de funções padrão, então você poderia dizer default operator==; Aprendemos que ele ajuda a fazer essas coisas explícito.

MSalters
fonte
3
Eu pensei que apenas as "funções-membro especiais" (construtor padrão, construtor de cópias, operador de atribuição e destruidor) pudessem ser explicitamente padronizadas. Eles estenderam isso para outros operadores?
22768 Michael Burr
4
O construtor Move também pode ter o padrão, mas acho que isso não se aplica operator==. O que é uma pena.
Pavel Minaev 29/10/09
5

Conceitualmente, não é fácil definir igualdade. Mesmo para dados POD, pode-se argumentar que, mesmo que os campos sejam iguais, mas é um objeto diferente (em um endereço diferente), não é necessariamente igual. Na verdade, isso depende do uso do operador. Infelizmente, seu compilador não é psíquico e não pode inferir isso.

Além disso, as funções padrão são excelentes maneiras de dar um tiro no próprio pé. Os padrões que você descreve estão basicamente lá para manter a compatibilidade com as estruturas de POD. No entanto, eles causam estragos mais do que suficiente, com os desenvolvedores esquecendo-os ou a semântica das implementações padrão.

Paul de Vrieze
fonte
10
Não há ambiguidade para estruturas de POD - elas devem se comportar exatamente da mesma maneira que qualquer outro tipo de POD, que é a igualdade de valor (em vez de referenciar a igualdade). Um intcriado via copiador de outro é igual àquele a partir do qual foi criado; a única coisa lógica a fazer para um structdos dois intcampos é trabalhar exatamente da mesma maneira.
Pavel Minaev 29/10/2009
1
@ mgiuca: Eu posso ver uma utilidade considerável para uma relação de equivalência universal que permitiria que qualquer tipo que se comporte como um valor seja usado como uma chave em um dicionário ou coleção semelhante. Tais coleções não podem se comportar de maneira útil sem uma relação de equivalência garantida-reflexiva. IMHO, a melhor solução seria definir um novo operador que todos os tipos internos pudessem implementar de maneira sensata e definir alguns novos tipos de ponteiros que eram semelhantes aos existentes, exceto que alguns definiriam a igualdade como equivalência de referência, enquanto outros se uniriam ao alvo operador de equivalência.
supercat
1
@supercat Por analogia, você pode apresentar quase o mesmo argumento para o +operador, pois não é associativo para carros alegóricos; isto é (x + y) + z! = x + (y + z), devido à maneira como ocorre o arredondamento FP. (Indiscutivelmente, esse é um problema muito pior do que ==porque é verdadeiro para valores numéricos normais.) Você pode sugerir a adição de um novo operador de adição que funcione para todos os tipos numéricos (mesmo int) e é quase exatamente o mesmo +que é associativo ( de alguma forma). Mas então você adicionaria inchaço e confusão ao idioma sem realmente ajudar tantas pessoas.
mgiuca
1
@mgiuca: Ter coisas bastante semelhantes, exceto em casos extremos, é frequentemente extremamente útil, e esforços equivocados para evitar tais coisas resultam em muita complexidade desnecessária. Se, às vezes, o código do cliente precisar que casos de borda sejam tratados de uma maneira e, às vezes, que eles sejam tratados de outra maneira, ter um método para cada estilo de tratamento eliminará muitos códigos de tratamento de casos de borda no cliente. Quanto à sua analogia, não há como definir a operação em valores de ponto flutuante de tamanho fixo para produzir resultados transitivos em todos os casos (embora algumas linguagens da década de 1980 tivessem melhor semântica ... #
23815
1
... do que hoje a esse respeito) e, portanto, o fato de que eles não fazem o impossível não deve ser uma surpresa. Não há obstáculo fundamental, no entanto, para implementar uma relação de equivalência que seria universalmente aplicável a qualquer tipo de valor que possa ser copiado.
Supercat
1

Existe uma boa razão para isso? Por que realizar uma comparação membro a membro seria um problema?

Pode não ser um problema funcional, mas em termos de desempenho, a comparação padrão por membro é passível de ser menos otimizada do que a atribuição / cópia padrão por membro. Diferentemente da ordem de atribuição, a ordem de comparação afeta o desempenho porque o primeiro membro desigual implica que o restante pode ser ignorado. Portanto, se existem alguns membros que geralmente são iguais, você deseja compará-los por último e o compilador não sabe quais são os membros com maior probabilidade de serem iguais.

Considere este exemplo, em que verboseDescriptionuma sequência longa é selecionada de um conjunto relativamente pequeno de possíveis descrições meteorológicas.

class LocalWeatherRecord {
    std::string verboseDescription;
    std::tm date;
    bool operator==(const LocalWeatherRecord& other){
        return date==other.date
            && verboseDescription==other.verboseDescription;
    // The above makes a lot more sense than
     // return verboseDescription==other.verboseDescription
     //     && date==other.date;
    // because some verboseDescriptions are liable to be same/similar
    }
}

(É claro que o compilador teria o direito de desconsiderar a ordem das comparações se reconhecer que elas não têm efeitos colaterais, mas, presumivelmente, ainda assim retiraria sua que do código-fonte, onde não possui informações melhores.)

Museful
fonte
Mas ninguém o impede de escrever uma comparação otimizada definida pelo usuário se você encontrar um problema de desempenho. Na minha experiência, isso seria uma minoria minúscula de casos.
Peter - Restabelece Monica
1

Apenas para que as respostas a esta pergunta permaneçam completas com o passar do tempo: desde C ++ 20, ele pode ser gerado automaticamente com o comando auto operator<=>(const foo&) const = default;

Ele irá gerar todos os operadores: ==,! =, <, <=,> E> =, consulte https://en.cppreference.com/w/cpp/language/default_comparisons para obter detalhes.

Devido à aparência do operador <=>, é chamado de operador de nave espacial. Veja também Por que precisamos do operador da nave espacial <=> em C ++? .

EDIT: também em C ++ 11 um belo substituto puro para que está disponível com std::tiever https://en.cppreference.com/w/cpp/utility/tuple/tie para um exemplo de código completo com bool operator<(…). A parte interessante, alterada para trabalhar, ==é:

#include <tuple>

struct S {
………
bool operator==(const S& rhs) const
    {
        // compares n to rhs.n,
        // then s to rhs.s,
        // then d to rhs.d
        return std::tie(n, s, d) == std::tie(rhs.n, rhs.s, rhs.d);
    }
};

std::tie funciona com todos os operadores de comparação e é completamente otimizado pelo compilador.

cosurgi
fonte
-1

Concordo que, para as classes do tipo POD, o compilador pode fazer isso por você. No entanto, o que você pode considerar simples, o compilador pode estar errado. Portanto, é melhor deixar o programador fazer isso.

Eu já tive um caso de POD em que dois campos eram únicos - portanto, uma comparação nunca seria considerada verdadeira. No entanto, a comparação que eu precisava apenas comparou na carga útil - algo que o compilador nunca entenderia ou poderia descobrir por si próprio.

Além disso - eles não demoram muito para escrever, não são ?!

graham.reeds
fonte