Dlaczego kompilatory C ++ nie definiują operatora == i operatora! =?


302

Jestem wielkim fanem pozwalania kompilatorowi na wykonanie dla ciebie jak największej pracy. Pisząc prostą klasę, kompilator może dać ci następujące za „za darmo”:

  • Domyślny (pusty) konstruktor
  • Konstruktor kopii
  • Destruktor
  • Operator przypisania ( operator=)

Ale wydaje się, że nie daje żadnych operatorów porównania - takich jak operator==lub operator!=. Na przykład:

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

Czy jest na to dobry powód? Dlaczego przeprowadzanie porównania poszczególnych członków byłoby problemem? Oczywiście, jeśli klasa przydzieli pamięć, to powinieneś być ostrożny, ale dla prostej klasy z pewnością kompilator mógłby to zrobić za Ciebie?


4
Oczywiście również destruktor jest udostępniany za darmo.
Johann Gerell,

23
W jednym ze swoich ostatnich wystąpień Alex Stepanov wskazał, że błędem było nie mieć domyślnego automatycznego ==, podobnie jak w przypadku domyślnych automatycznych przypisań ( =). (Argument o wskaźnikach jest niespójna, ponieważ logika ma zastosowanie zarówno do =i ==, a nie tylko na sekundę).
alfC

2
@becko Jest to jedna z serii na A9: youtube.com/watch?v=k-meLQaYP5Y , nie pamiętam, w której z rozmów. Istnieje również propozycja, że ​​wydaje się, że trafia do C ++ 17 open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0221r0.html
alfC

1
@becko, jest jednym z pierwszych w serii „Efektywne programowanie z komponentami” lub „Rozmowy programistyczne” w A9, dostępnej na Youtube.
alfC

1
@becko Właściwie poniżej znajduje się odpowiedź wskazująca punkt widzenia Alexa stackoverflow.com/a/23329089/225186
alfC

Odpowiedzi:


71

Kompilator nie będzie wiedział, czy chcesz porównanie wskaźnika czy głębokie (wewnętrzne) porównanie.

Bezpieczniej jest po prostu nie wdrożyć go i pozwolić programiście zrobić to sam. Następnie mogą dokonać wszystkich założeń, które im się podobają.


292
Ten problem nie powstrzymuje go przed wygenerowaniem ctor kopiowania, gdzie jest dość szkodliwy.
MSalters,

78
Kopiowanie konstruktorzy (i operator=) ogólnie pracy w tym samym kontekście jak operatorów porównania - to znaczy, że jest oczekiwanie, że po wykonaniu a = b, a == bjest prawdą. Zdecydowanie sensowne jest, aby kompilator zapewnił wartość domyślną operator==przy użyciu tej samej semantyki wartości zagregowanych, jak w przypadku operator=. Podejrzewam, że paercebal ma tutaj rację, ponieważ operator=(i kopiuj ctor) są dostarczane wyłącznie dla kompatybilności z C i nie chcieli pogorszyć sytuacji.
Pavel Minaev

46
-1. Oczywiście chcesz głębokiego porównania, jeśli programista chciałby porównania wskaźników, napisałby (& f1 == & f2)
Viktor Sehr

62
Viktor, sugeruję, abyś ponownie przemyślał swoją odpowiedź. Jeśli klasa Foo zawiera pasek *, to skąd kompilator wiedziałby, czy Foo :: operator == chce porównać adres paska * lub zawartość paska?
Mark Ingram

46
@ Mark: Jeśli zawiera wskaźnik, porównanie wartości wskaźnika jest rozsądne - jeśli zawiera wartość, porównanie wartości jest rozsądne. W wyjątkowych okolicznościach programista może zastąpić. To tak, jakby język implementuje porównanie ints i wskaźnik-int-ints.
Eamon Nerbonne,

317

Argument, że jeśli kompilator może zapewnić domyślny konstruktor kopii, powinien być w stanie zapewnić podobny domyślny konstruktor operator==() ma pewien sens. Myślę, że powód podjęcia decyzji o niedostarczeniu generatora kompilatora dla tego operatora można odgadnąć na podstawie tego, co Stroustrup powiedział o domyślnym konstruktorze kopii w „The Design and Evolution of C ++” (Rozdział 11.4.1 - Kontrola kopiowania) :

Osobiście uważam za niefortunne, że operacje kopiowania są domyślnie zdefiniowane i zabraniam kopiowania obiektów wielu moich klas. Jednak C ++ odziedziczył domyślne C i konstruktory kopiowania po C i są one często używane.

Zamiast „dlaczego C ++ nie ma wartości domyślnej operator==() ?”, Pytanie powinno brzmieć „dlaczego C ++ ma domyślny konstruktor przypisania i kopiowania?”, Przy czym odpowiedź jest taka, że ​​Stroustrup niechętnie uwzględnił te elementy w celu zapewnienia wstecznej zgodności z C (prawdopodobnie przyczyną większości brodawek C ++, ale także prawdopodobnie główną przyczyną popularności C ++).

Na własne potrzeby w moim IDE fragment kodu, którego używam dla nowych klas, zawiera deklaracje dla prywatnego operatora przypisania i konstruktora kopiowania, więc gdy generuję nową klasę, nie otrzymuję żadnych domyślnych operacji przypisania i kopiowania - muszę jawnie usunąć deklarację tych operacji z private:sekcji, jeśli chcę, aby kompilator mógł je dla mnie wygenerować.


29
Dobra odpowiedź. Chciałbym tylko zaznaczyć, że w C ++ 11 zamiast uczynić operatora przypisania i konstruktora kopiowania prywatnymi, można je całkowicie usunąć w następujący sposób: Foo(const Foo&) = delete; // no copy constructoriFoo& Foo=(const Foo&) = delete; // no assignment operator
karadoc

9
„Jednak C ++ odziedziczyło swoje domyślne przypisanie i kopiuje konstruktory z C” To nie oznacza, że ​​musisz w ten sposób tworzyć WSZYSTKIE typy C ++. Powinny ograniczyć to do zwykłych starych POD, tylko tych, które są już w C, nie więcej.
thesaint

3
Z pewnością rozumiem, dlaczego C ++ odziedziczył te zachowania struct, ale chciałbym, aby classzachowywał się inaczej (i zdrowo). W tym procesie dałoby to również bardziej znaczącą różnicę między domyślnym dostępem structa classdostępem obok niego.
jamesdlin

@jamesdlin Jeśli chcesz regułę, najbardziej sensowne byłoby wyłączenie niejawnej deklaracji i definicji lekarzy i przydziału, jeśli zadeklarowany jest dtor.
Deduplicator

1
Nadal nie widzę żadnej szkody, pozwalając programistom na jawne zamówienie kompilatora do utworzenia operator==. W tym momencie jest to tylko cukier składniowy dla jakiegoś kodu płyty kotłowej. Jeśli obawiasz się, że w ten sposób programista może przeoczyć jakiś wskaźnik wśród pól klas, możesz dodać warunek, że może on działać tylko na prymitywnych typach i obiektach, które same mają operatory równości. Nie ma jednak powodu, aby całkowicie to odrzucać.
NO_NAME

93

Nawet w C ++ 20 kompilator nadal nie będzie generował operator==dla ciebie domyślnie

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

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

Ale zyskasz możliwość jawnego domyślnego działania == od 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;
};

Domyślne ustawienie ==działa w zależności od członka ==(w taki sam sposób, jak domyślny konstruktor kopiuje w przypadku tworzenia kopii w zależności od członka). Nowe zasady zapewniają również oczekiwany związek między ==i !=. Na przykład dzięki powyższej deklaracji mogę napisać oba:

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

Ta szczególna funkcja (domyślna operator==i symetria pomiędzy ==i !=) pochodzi z jednej propozycji, która była częścią szerszej funkcji języka, którą jest operator<=>.


Czy wiesz, czy jest w tym jakaś nowsza aktualizacja? Czy będzie dostępny w c ++ 17?
dcmm88,

3
@ dcmm88 Niestety nie będzie dostępny w C ++ 17. Zaktualizowałem odpowiedź.
Anton Savin,

2
Zmodyfikowana propozycja, która pozwala na to samo (oprócz krótkiej formy), będzie w C ++ 20 :)
Rakete1111

Więc w zasadzie musisz określić = default, dla rzeczy, która nie jest tworzona domyślnie, prawda? Dla mnie to brzmi jak oksymoron („wyraźne domyślne”).
Artin

@artin Ma to sens, ponieważ dodawanie nowych funkcji do języka nie powinno przerywać istniejącej implementacji. Dodanie nowych standardów bibliotecznych lub nowych rzeczy, które kompilator może zrobić, to jedno. Dodanie nowych funkcji składowych tam, gdzie wcześniej nie istniały, to zupełnie inna historia. Aby zabezpieczyć projekt przed błędami, wymagałoby to znacznie więcej wysiłku. Osobiście wolałbym flagę kompilatora, aby przełączać się między jawnymi i domyślnymi domyślnymi. Budujesz projekt ze starszego standardu C ++, użyj jawnej domyślnej flagi kompilatora. Już zaktualizowałeś kompilator, więc powinieneś go poprawnie skonfigurować. W przypadku nowych projektów uczyń to domyślnym.
Maciej Załucki

44

IMHO, nie ma „dobrego” powodu. Powodem, dla którego tak wielu ludzi zgadza się z tą decyzją projektową, jest to, że nie nauczyli się opanowywać potęgi semantyki opartej na wartościach. Ludzie muszą napisać wiele niestandardowych konstruktorów kopiowania, operatorów porównania i destruktorów, ponieważ używają surowych wskaźników w swojej implementacji.

Przy stosowaniu odpowiednich inteligentnych wskaźników (takich jak std :: shared_ptr) domyślny konstruktor kopii jest zwykle w porządku, a oczywista implementacja hipotetycznego domyślnego operatora porównania byłaby równie dobra.


39

Odpowiedź: C ++ nie zrobił ==, ponieważ C nie, a oto dlaczego C zapewnia tylko domyślną =, ale nie == na pierwszym miejscu. C chciał uprościć: C zaimplementowane = przez memcpy; jednak == nie może zostać zaimplementowane przez memcmp z powodu wypełniania. Ponieważ padding nie jest inicjowany, memcmp mówi, że są różne, mimo że są takie same. Ten sam problem istnieje w przypadku pustych klas: memcmp mówi, że są one różne, ponieważ wielkość pustych klas nie jest równa zero. Z góry widać, że implementacja == jest bardziej skomplikowana niż implementacja = w C. Niektóre przykłady kodu dotyczące tego. Twoja poprawka jest mile widziana, jeśli się mylę.


6
C ++ nie używa memcpy dla operator=- działałoby to tylko dla typów POD, ale C ++ zapewnia domyślną również operator=dla typów innych niż POD.
Flexo

2
Tak, C ++ zaimplementowano = w bardziej wyrafinowany sposób. Wygląda na to, że C właśnie zaimplementowano = z prostym memcpy.
Rio Wing,

Treść tej odpowiedzi powinna być połączona z odpowiedzią Michaela. Poprawia pytanie, a następnie odpowiada na nie.
Sgene9,

27

W tym filmie Alex Stepanov, twórca STL, odpowiada na to pytanie około godziny 13:00. Podsumowując, obserwując ewolucję C ++, twierdzi, że:

  • Szkoda, że == i! = Nie są niejawnie zadeklarowane (a Bjarne się z nim zgadza). Właściwy język powinien mieć te rzeczy gotowe (dalej sugeruje, że nie powinieneś być w stanie zdefiniować ! = , Który łamie semantykę == )
  • Przyczyna tego przypadku ma swoje korzenie (jak wiele problemów w C ++) w C. Tam operator przypisania jest domyślnie definiowany za pomocą przypisania krok po kroku, ale to nie działałoby dla == . Bardziej szczegółowe wyjaśnienie można znaleźć w tym artykule Bjarne Stroustrup.
  • W kolejnym pytaniu, dlaczego nie wykorzystano członka porównania członków , mówi niesamowitą rzecz : C był rodzimym językiem, a facet wdrażający te rzeczy dla Ritchie powiedział mu, że jest to trudne do wdrożenia!

Następnie mówi, że w (odległej) przyszłości == i ! = Zostaną domyślnie wygenerowane.


2
wygląda na to, że ta odległa przyszłość nie będzie w 2017 ani 18, ani 19, więc złapiesz mój dryf ...
UmNyobe

18

C ++ 20 zapewnia sposób na łatwą implementację domyślnego operatora porównania.

Przykład z 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<=>

4
Dziwię się, że posłużyły one Pointza przykład operacji zamawiania , ponieważ nie ma rozsądnego domyślnego sposobu zamówienia dwóch punktów xi ywspółrzędnych ...
potok

4
@pipe Jeśli nie ma znaczenia, w jakiej kolejności są elementy, użycie domyślnego operatora ma sens. Na przykład możesz użyć, std::setaby upewnić się, że wszystkie punkty są unikalne i std::setużywa operator<tylko.
vll

O rodzaju zwrotu auto: W takim przypadku zawsze możemy założyć, że będzie on std::strong_orderingpochodzić #include <compare>?
kevinarpe

1
@kevinarpe Zwracany jest typ std::common_comparison_category_t, który dla tej klasy staje się domyślną kolejnością ( std::strong_ordering).
vll

15

Nie można zdefiniować wartości domyślnej ==, ale można zdefiniować wartość domyślną, !=za pomocą ==której zwykle należy się zdefiniować. W tym celu należy wykonać następujące czynności:

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

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

Szczegółowe informacje można znaleźć na stronie http://www.cplusplus.com/reference/std/utility/rel_ops/ .

Ponadto, jeśli zdefiniujesz operator< , operatory dla <=,>,> = można z niego wywnioskować podczas używania std::rel_ops.

Należy jednak zachować ostrożność podczas korzystania, std::rel_opsponieważ operatory porównania można wywnioskować dla typów, których się nie oczekuje.

Bardziej preferowanym sposobem na wyciągnięcie powiązanego operatora z podstawowego jest użycie boost :: operators .

Podejście zastosowane w boost jest lepsze, ponieważ definiuje użycie operatora dla klasy, którą chcesz, a nie dla wszystkich klas w zakresie.

Możesz również wygenerować „+” z „+ =”, - z „- =” itd. (Zobacz pełną listę tutaj )


Nie dostałem domyślnego !=po napisaniu ==operatora. Albo ja to zrobiłem, ale brakowało jej const. Sam też musiałem to napisać i wszystko było dobrze.
Jan

możesz grać z konsekwencją, aby osiągnąć pożądane wyniki. Bez kodu trudno jest powiedzieć, co jest z nim nie tak.
sergtk

2
W rel_opsC ++ 20 jest nieaktualny powód : ponieważ nie działa , przynajmniej nie wszędzie, a na pewno nie konsekwentnie. Nie ma niezawodnego sposobu sort_decreasing()na kompilację. Z drugiej strony Boost.Operators działa i zawsze działał.
Barry

10

C ++ 0x ma propozycję domyślnych funkcji, więc można powiedzieć, default operator==; że dowiedzieliśmy się, że pomaga to wyjaśnić te rzeczy.


3
Pomyślałem, że tylko „specjalne funkcje składowe” (domyślny konstruktor, konstruktor kopiujący, operator przypisania i destruktor) mogą zostać jawnie przywrócone. Czy rozszerzyli to na niektórych innych operatorów?
Michael Burr

4
Konstruktor ruchu może być również ustawiony domyślnie, ale nie sądzę, że dotyczy to operator==. Szkoda.
Pavel Minaev

5

Koncepcyjnie nie jest łatwo zdefiniować równość. Nawet w przypadku danych POD można argumentować, że nawet jeśli pola są takie same, ale jest to inny obiekt (pod innym adresem), niekoniecznie jest on równy. To zależy od sposobu użytkowania operatora. Niestety twój kompilator nie jest medium i nie może tego wywnioskować.

Poza tym domyślne funkcje to doskonały sposób na zastrzelenie się w stopę. Opisane wartości domyślne mają zasadniczo na celu zachowanie zgodności ze strukturami POD. Powodują one jednak więcej niż wystarczające spustoszenie, gdy programiści zapominają o nich lub semantykę domyślnych implementacji.


10
Nie ma dwuznaczności dla struktur POD - powinny one zachowywać się dokładnie tak samo, jak każdy inny typ POD, czyli równość wartości (zamiast równości odniesienia). Jeden intutworzony za pomocą kopiowania ctor z drugiego jest równy temu, z którego został utworzony; jedyną logiczną rzeczą dla jednego structz dwóch intpól jest działanie dokładnie w ten sam sposób.
Pavel Minaev

1
@mgiuca: Widzę znaczną przydatność dla uniwersalnej relacji równoważności, która pozwoliłaby na użycie dowolnego typu, który zachowuje się jak wartość, jako klucza w słowniku lub podobnej kolekcji. Takie kolekcje nie mogą jednak zachowywać się skutecznie bez relacji równoważności gwarantowanej-zwrotnej. IMHO, najlepszym rozwiązaniem byłoby zdefiniowanie nowego operatora, który wszystkie wbudowane typy mogłyby rozsądnie zaimplementować, i zdefiniowanie niektórych nowych typów wskaźników, które byłyby podobne do istniejących, z tym że niektóre zdefiniowałyby równość jako równoważność odniesienia, podczas gdy inne byłyby powiązane z celem operator równoważności.
supercat,

1
@ superupat Przez analogię można podać prawie taki sam argument dla +operatora, ponieważ nie jest on skojarzony z liczbami zmiennoprzecinkowymi ; to znaczy (x + y) + z! = x + (y + z), ze względu na sposób zaokrąglania FP. (Prawdopodobnie jest to o wiele gorszy problem niż ==dlatego, że dotyczy to normalnych wartości liczbowych). Możesz zasugerować dodanie nowego operatora dodawania, który działa dla wszystkich typów liczbowych (nawet int) i jest prawie dokładnie taki sam, +ale jest skojarzony ( jakoś). Ale wtedy dodawalibyście wzdęci i zamieszania do języka, nie pomagając tak wielu ludziom.
mgiuca,

1
@mgiuca: Posiadanie rzeczy, które są dość podobne, z wyjątkiem przypadków skrajnych, jest często bardzo przydatne, a błędne wysiłki mające na celu uniknięcie takich rzeczy powodują niepotrzebną złożoność. Jeśli kod klienta będzie czasem potrzebował obsługiwać przypadki na krawędziach w jeden sposób, a czasem wymagać ich obsługi w inny sposób, zastosowanie metody dla każdego stylu obsługi wyeliminuje wiele kodu obsługi przypadków na krawędziach w kliencie. Jeśli chodzi o twoją analogię, nie ma sposobu, aby zdefiniować operację na liczbach zmiennoprzecinkowych o stałej wielkości, aby uzyskać przechodnie wyniki we wszystkich przypadkach (chociaż niektóre języki lat 80. miały lepszą semantykę ...
supercat

1
... niż dzisiejszy pod tym względem), a zatem fakt, że nie robią niemożliwego, nie powinien być zaskoczeniem. Nie ma jednak podstawowej przeszkody we wdrażaniu relacji równoważności, która byłaby powszechnie stosowana do każdego rodzaju wartości, którą można skopiować.
supercat

1

Czy jest na to dobry powód? Dlaczego przeprowadzanie porównania poszczególnych członków byłoby problemem?

Może to nie być problem funkcjonalnie, ale pod względem wydajności domyślne porównanie poszczególnych członków może być bardziej nieoptymalne niż domyślne przypisanie / kopiowanie poszczególnych członków. W przeciwieństwie do kolejności przypisywania, kolejność porównywania wpływa na wydajność, ponieważ pierwszy nierówny element oznacza, że ​​resztę można pominąć. Więc jeśli istnieją elementy, które są zwykle równe, chcesz je porównać na końcu, a kompilator nie wie, które elementy są bardziej prawdopodobne.

Rozważ ten przykład, gdzie verboseDescriptionjest długi ciąg wybrany ze stosunkowo niewielkiego zestawu możliwych opisów pogody.

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

(Oczywiście kompilator byłby uprawniony do zignorowania kolejności porównań, gdyby rozpoznał, że nie mają one żadnych skutków ubocznych, ale przypuszczalnie nadal pobierałby swoją kolejkę od kodu źródłowego, w którym nie ma lepszych informacji).


Ale nikt nie powstrzymuje cię przed napisaniem optymalnego porównania zdefiniowanego przez użytkownika, jeśli znajdziesz problem z wydajnością. Z mojego doświadczenia wynika, że ​​byłaby to niewielka mniejszość przypadków.
Peter - Przywróć Monikę

1

Wystarczy, że odpowiedzi na to pytanie pozostaną kompletne w miarę upływu czasu: od C ++ 20 można go automatycznie wygenerować za pomocą polecenia auto operator<=>(const foo&) const = default;

Wygeneruje wszystkie operatory: ==,! =, <, <=,> I> =, patrz https://en.cppreference.com/w/cpp/language/default_comparisons celu uzyskania szczegółowych informacji.

Ze względu na wygląd operatora <=>nazywany jest operatorem statku kosmicznego. Zobacz także Dlaczego potrzebujemy operatora statku kosmicznego <=> w C ++?.

EDIT: również w C ++ 11 całkiem schludny substytutem, który jest dostępny z std::tiezobaczyć https://en.cppreference.com/w/cpp/utility/tuple/tie dla kompletnego przykład kodu z bool operator<(…). Interesującą częścią, zmienioną do pracy, ==jest:

#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 działa ze wszystkimi operatorami porównania i jest całkowicie zoptymalizowany przez kompilator.


-1

Zgadzam się, w przypadku klas typu POD kompilator mógłby zrobić to za Ciebie. Jednak to, co możesz uznać za proste, kompilator może się nie powieść. Lepiej więc pozwolić programiście to zrobić.

Miałem kiedyś przypadek POD, w którym dwa pola były unikalne - więc porównanie nigdy nie będzie uważane za prawdziwe. Jednak porównanie, którego potrzebowałem, porównywałem tylko ładunek - coś, czego kompilator nigdy nie zrozumie lub nigdy nie będzie w stanie samodzielnie zrozumieć.

Poza tym - nie trzeba długo pisać, prawda ?!

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.