Чому компілятори C ++ не визначають operator == та operator! =?


302

Я великий фанат, щоб дозволити компілятору зробити якомога більше роботи за вас. Коли ви пишете простий клас, компілятор може дати вам наступне для "безкоштовно":

  • Конструктор за замовчуванням (порожній)
  • Конструктор копій
  • Руйнівник
  • Оператор призначення ( operator=)

Але це, здається, не дає вам порівняльних операторів - таких як operator==або operator!=. Наприклад:

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

Чи є для цього вагома причина? Чому проведення порівняння по одному члену може бути проблемою? Очевидно, якщо клас виділяє пам'ять, то ви хочете бути обережними, але для простого класу напевно компілятор міг би це зробити для вас?


4
Звичайно, також деструктор надається безкоштовно.
Йоганн Герелл

23
В одному зі своїх останніх переговорів Олексій Степанов зазначав, що помилкою було не мати автоматичного за замовчуванням ==, таким же чином, як і автоматичне призначення за замовчуванням =за певних умов. (Аргумент про покажчиках суперечливий , тому що логіка може бути застосована як для =і ==, а не тільки для другого).
alfC

2
@becko Це один із серій на A9: youtube.com/watch?v=k-meLQaYP5Y , я не пам'ятаю, в якому з переговорів. Також є пропозиція, яка, схоже, пробивається до C ++ 17 open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0221r0.html
alfC

1
@becko, це одна з перших або в серії "Ефективне програмування з компонентами", або "Бесіди з програмуванням" на A9, доступній в Youtube.
alfC

1
@becko На насправді є відповідь нижче , вказує на точку Алекса зору stackoverflow.com/a/23329089/225186
alfC

Відповіді:


71

Компілятор не знав, чи хочете ви порівняння вказівника чи глибокого (внутрішнього) порівняння.

Безпечніше просто не реалізувати це, а дозволити програмісту зробити це самостійно. Тоді вони можуть зробити всі припущення, які їм подобаються.


292
Ця проблема не заважає генерувати копіюючий копій, де це досить шкідливо.
MSalters

78
Конструктори копіювання (і operator=) зазвичай працюють в тому ж контексті, оператори порівняння - тобто, є надія , що після виконання a = b, a == bвірно. Звичайно, компілятор має сенс надати за замовчуванням, operator==використовуючи ту саму семантику сукупного значення, що і для operator=. Я підозрюю, що паєрцебал справді правильний, оскільки operator=(і копіюючий ctor) надається виключно для сумісності з C, і вони не хотіли погіршити ситуацію.
Павло Мінаєв

46
-1. Звичайно, ви хочете глибокого порівняння, якби програміст хотів порівняння вказівника, він напише (& f1 == & f2)
Viktor Sehr

62
Вікторе, я пропоную вам переосмислити свою відповідь. Якщо клас Foo містить бар *, то як би компілятор дізнався, чи хоче Foo :: operator == порівняти адресу Bar * або вміст Bar?
Позначити Інграма

46
@Mark: Якщо він містить вказівник, порівняння значень вказівника є розумним - якщо воно містить значення, порівняння значень є розумним. У виняткових обставинах програміст може перекрити. Це подібно до того, що мова реалізує порівняння між int та вказівниками на int.
Еймон Нербонна

317

Аргумент того, що якщо компілятор може надати конструктор копій за замовчуванням, він повинен бути в змозі надати аналогічний за замовчуванням operator==()має певний сенс. Я думаю, що про причину рішення не надавати компілятор, створений компілятором для цього оператора, можна здогадатися, що Stroustrup сказав про конструктор копій за замовчуванням у "Дизайн та еволюція C ++" (Розділ 11.4.1 - Контроль копіювання) :

Особисто я вважаю прикро, що операції з копіюванням визначені за замовчуванням, і я забороняю копіювати об'єкти багатьох моїх класів. Однак C ++ успадкував конструктори присвоєння та копіювання за замовчуванням від C, і вони часто використовуються.

Тож замість "чому у C ++ немає за замовчуванням operator==()?", Питання повинне було бути "чому C ++ має конструктор присвоєння за замовчуванням та конструктор копій?", У відповіді на те, що ці елементи були включені неохоче Stroustrup для зворотної сумісності з C (можливо, причиною більшості бородавок C ++, але також, ймовірно, основною причиною популярності C ++).

Для моїх власних цілей у моєму IDE фрагмент, який я використовую для нових класів, містить декларації для приватного оператора присвоєння та конструктора копіювання, так що коли я створюю новий клас, я не отримую операцій з призначення та копіювання за замовчуванням - я повинен видалити декларацію тих операцій з private:розділу, якщо я хочу, щоб компілятор міг їх генерувати для мене.


29
Хороша відповідь. Я просто хотів би зазначити, що в C ++ 11, замість того, щоб робити оператора призначення та конструктора копіювання приватними, ви можете видалити їх повністю так: Foo(const Foo&) = delete; // no copy constructorіFoo& Foo=(const Foo&) = delete; // no assignment operator
karadoc

9
"Однак C ++ успадкував свої конструктори за замовчуванням і скопіювати конструктори від C" Це не означає, чому вам потрібно робити ВСІ типи C ++ таким чином. Вони повинні були просто обмежити це звичайними старими ПОД, лише типи, які вже є у С, не більше.
святого

3
Я, безумовно, можу зрозуміти, чому C ++ успадкував цю поведінку struct, але я хочу, щоб це не classмало поводитися по-різному (і здорово). У ході цього процесу він також дав би більш змістовну різницю між доступом за замовчуванням structта classпоряд з ним.
jamesdlin

@jamesdlin Якщо потрібно правило, відключення неявного декларування та визначення ctors та призначення, якщо оголошено dtor, матиме найбільше значення.
Дедуплікатор

1
Я все ще не бачу ніякої шкоди в тому, щоб дозволити програмісту явно наказати компілятору створити operator==. На даний момент це лише синтаксичний цукор для деякого кодового коду. Якщо ви боїтесь, що таким чином програміст може не помітити деякий вказівник серед класових полів, ви можете додати умову, що він може працювати лише над примітивними типами та об'єктами, які мають самі оператори рівності. Однак немає ніяких причин цілком забороняти це.
NO_NAME

93

Навіть у C ++ 20 компілятор все одно не буде неявно генерувати operator==для вас

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

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

Але ви отримаєте можливість явного дефолту == після 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;
};

Дефолт ==робить членом ==(так само, як конструктор копій за замовчуванням виконує конструювання копій для членів). Нові правила також передбачають очікувані відносини між ==та !=. Наприклад, за допомогою заяви вище, я можу написати і те, і інше:

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

Ця специфічна особливість (дефолт operator==та симетрія між ==та !=) походить від однієї пропозиції, яка була частиною більш широкої мовної функції operator<=>.


Чи знаєте ви, чи є новіші оновлення щодо цього? Це буде доступне в c ++ 17?
dcmm88

3
@ dcmm88 На жаль, це не буде доступно в C ++ 17. Я оновив відповідь.
Антон Савін

2
Змінена пропозиція, яка дозволяє те ж саме (крім короткої форми), буде в С ++ 20, хоча :)
Rakete1111,

Тому в основному вам потрібно вказати = default, для речі, яка не створена за замовчуванням, правда? Мені це звучить як оксиморон ("явний дефолт").
artin

@artin Це має сенс, оскільки додавання нових функцій до мови не повинно порушувати існуючу реалізацію. Додавання нових стандартів бібліотеки або компілятора нових речей може зробити одне. Додавання нових функцій членів там, де їх раніше не було, - це зовсім інша історія. Щоб убезпечити свій проект від помилок, знадобиться набагато більше зусиль. Я особисто вважаю за краще прапор компілятора перемикатися між явними та неявними за замовчуванням. Ви будуєте проект із більш старого стандарту C ++, використовуєте явний за замовчуванням прапор компілятора. Ви вже оновлюєте компілятор, тож слід правильно його налаштувати. Для нових проектів зробити це неявним.
Мацей Залуцький

44

ІМХО, "доброї" причини немає. Причина, що так багато людей згодні з цим дизайнерським рішенням, полягає в тому, що вони не навчилися оволодіти силою ціннісної семантики. Людям потрібно писати багато спеціальних конструкторів копій, операторів порівняння та деструкторів, оскільки вони використовують необмежену вказівку для своєї реалізації.

При використанні відповідних інтелектуальних покажчиків (наприклад, std :: shared_ptr) конструктор копій за замовчуванням зазвичай прекрасний, і очевидна реалізація гіпотетичного оператора порівняння за замовчуванням була б такою ж чудовою.


39

На нього відповіли, що C ++ не зробив == тому, що C не зробив, і ось чому C забезпечує лише за замовчуванням =, але ні == на першому місці. C хотів зробити це просто: C реалізовано = memcpy; однак, == не може бути реалізований memcmp через прокладку. Оскільки прокладка не ініціалізована, memcmp говорить, що вони різні, хоча вони однакові. Така ж проблема існує і для порожнього класу: memcmp говорить, що вони різні, оскільки розмір порожніх класів не дорівнює нулю. Зверху видно, що реалізація == є складнішою, ніж реалізація = в C. Деякі приклади коду щодо цього. Ваша корекція оцінюється, якщо я помиляюся.


6
C ++ не використовує memcpy для operator=- це буде працювати лише для типів POD, але C ++ надає за замовчуванням і operator=для інших типів POD.
Flexo

2
Так, C ++ реалізовано = більш досконалим способом. Здається, що C просто реалізований = за допомогою простого memcpy.
Крило Ріо

Зміст цієї відповіді слід скласти разом із Майклом. Він виправляє питання, після чого відповідає на нього.
Sgene9

27

У цьому відео Олексій Степанов, автор STL, вирішує саме це питання близько 13:00. Підводячи підсумок, спостерігаючи за розвитком C ++, він стверджує, що:

  • Прикро, що == і! = Неявно не оголошені (і Б'ярн погоджується з ним). Правильна мова повинна бути готова для вас (він продовжує підказувати, що ви не зможете визначити ! =, Що порушує семантику == )
  • Причиною цього є коріння (стільки ж проблем C ++) у C. Там оператор присвоєння неявно визначений з бітовим призначенням, але це не працює для == . Більш детальне пояснення можна знайти в цій статті від Bjarne Stroustrup.
  • У подальшому запитанні Чому тоді не був членом по використанню порівняння членів, він говорить дивовижну річ : C був якось домашньою мовою, і хлопець, що реалізував ці речі для Річі, сказав йому, що він вважає, що це важко реалізувати!

Потім він каже, що в (далекому) майбутньому = неявно буде генеруватися == і ! = .


2
здається, що це далеке майбутнє не буде ні 2017, ні 18, ні 19, ну ви
спіймаєте

18

C ++ 20 пропонує спосіб легко реалізувати оператор порівняння за замовчуванням.

Приклад із 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
Я здивований, що вони використовувались Pointяк приклад для операції замовлення , оскільки немає розумного способу замовити два пункти xі yкоординати ...
труба

4
@pipe Якщо вам не важливо, в якому порядку знаходяться елементи, використання оператора за замовчуванням має сенс. Наприклад, ви можете використовувати, std::setщоб переконатися, що всі точки є унікальними та std::setвикористовуються operator<лише.
vll

Про тип повернення auto: чи можна у цьому випадку завжди припускати, що це буде std::strong_orderingз #include <compare>?
kevinarpe

1
@kevinarpe Тип повернення є std::common_comparison_category_t, що для цього класу стає замовленням за замовчуванням ( std::strong_ordering).
vll

15

Неможливо визначити за замовчуванням ==, але ви можете визначити дефолт, за !=допомогою ==якого зазвичай слід визначити себе. Для цього слід зробити наступні дії:

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

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

Докладні відомості можна знайти на веб-сайті http://www.cplusplus.com/reference/std/utility/rel_ops/ .

Крім того, якщо ви визначите operator< , оператори для <=,>,> = можуть бути виведені з нього при використанні std::rel_ops.

Але ви повинні бути обережними при використанні, std::rel_opsоскільки оператори порівняння можуть бути виведені для типів, від яких ви не очікуєте.

Більш бажаний спосіб вивести пов'язаного оператора з базового - використовувати boost :: оператори .

Підхід, що використовується в boost, є кращим, оскільки він визначає використання оператора лише для класу, який ви хочете, а не для всіх класів за обсягом.

Ви також можете генерувати "+" з "+ =", - з "- =" тощо тощо (див. Повний список тут )


Я не отримав дефолт !=після написання ==оператора. Або я це зробив, але це не бракувало const. Довелося це теж написати, і все було добре.
Джон

Ви можете грати з конкурентоспроможністю для досягнення необхідних результатів. Без коду важко сказати, що з цим не так.
сергтк

2
У rel_opsC ++ 20 причина застаріла: адже вона не працює , принаймні не скрізь, і, звичайно, не послідовно. Немає надійного способу отримати sort_decreasing()компіляцію. З іншого боку, Boost.Operators працює і завжди працював.
Баррі

10

C ++ 0x має була пропозиція за замовчуванням функцій, так що ви могли б сказати , default operator==; ми дізналися , що це допомагає зробити ці речі явно.


3
Я думав, що лише "спеціальні функції членів" (конструктор за замовчуванням, конструктор копій, оператор присвоєння та деструктор) можуть бути явно дефолтовані. Чи поширили це це на деяких інших операторів?
Майкл Берр

4
Конструктор переміщення також може бути дефолтним, але я не думаю, що це стосується operator==. Що шкода.
Павло Мінаєв

5

Концептуально визначити рівність непросто. Навіть для даних POD можна стверджувати, що навіть якщо поля однакові, але це різний об'єкт (за різною адресою), він не обов'язково дорівнює. Це фактично залежить від використання оператора. На жаль, ваш компілятор не психічний і не може цього зробити.

Окрім цього, функції за замовчуванням - це відмінний спосіб стріляти в ногу. Описані за замовчуванням в основному існують для збереження сумісності зі структурами POD. Однак вони завдають більш ніж достатнього хаосу, коли розробники забувають про них, або семантику реалізацій за замовчуванням.


10
Для структур POD немає ніякої неоднозначності - вони повинні поводитись так само, як це робить і будь-який інший тип POD, а це значення величини (швидше, ніж еталонна рівність). Один, intстворений через копіюючий ctor з іншого, дорівнює тому, з якого він створений; єдине логічне, що потрібно зробити для structдвох intполів - це працювати точно так само.
Павло Мінаєв

1
@mgiuca: Я бачу значну корисність для універсального відношення еквівалентності, яке дозволило б будь-який тип, що поводиться як значення, використовувати як ключ у словнику чи подібній колекції. Однак такі колекції не можуть вести себе корисно без гарантовано-рефлексивного відношення еквівалентності. ІМХО, найкращим рішенням було б визначити нового оператора, який усі вбудовані типи могли б розумно реалізувати, та визначити деякі нові типи вказівників, які були подібні до існуючих, за винятком того, що деякі визначали б рівність як еквівалентність еталону, а інші ланцюгають до цільового оператор еквівалентності
supercat

1
@supercat За аналогією ви можете зробити такий же аргумент для +оператора, що він не асоціативний для плавців; тобто (x + y) + z! = x + (y + z), завдяки способу округлення FP. (Можливо, це набагато гірша проблема, ніж ==тому, що це стосується звичайних числових значень.) Ви можете запропонувати додати новий оператор додавання, який працює для всіх числових типів (навіть int) і майже точно такий же, як, +але він асоціативний ( якось). Але тоді ви б додали мови і плутанини в мові, не допомагаючи дуже багатьом.
mgiuca

1
@mgiuca: Наявність подібних речей, за винятком крайових випадків, часто є надзвичайно корисним, а неправильні зусилля, щоб уникнути подібних речей, призводять до дуже непотрібної складності. Якщо клієнтському коду іноді знадобиться обробка крайових випадків в одну сторону, а іноді потрібна їх обробка іншим способом, для кожного способу обробки метод усуне багато клієнтського коду оброблення кейсів. Що стосується вашої аналогії, то немає способу визначити роботу з значеннями з плаваючою комою фіксованого розміру, щоб отримати транзитивні результати у всіх випадках (хоча деякі мови 1980-х років мали кращу семантику ...
supercat

1
... ніж сьогодні в цьому плані), і тому те, що вони не роблять неможливе, не повинно бути несподіванкою. Однак немає принципової перешкоди для реалізації відношення еквівалентності, яке було б загальноприйнятним для будь-якого типу цінностей, які можна скопіювати.
supercat

1

Чи є для цього вагома причина? Чому проведення порівняння по одному члену може бути проблемою?

Це може не бути проблемою функціонально, але з точки зору продуктивності порівняння за замовчуванням член-член може бути більш неоптимальним, ніж призначення / копіювання за замовчуванням член-член-член. На відміну від порядку призначення, порядок порівняння впливає на результативність, оскільки перший нерівний член передбачає, що решта може бути пропущена. Тож якщо є деякі члени, які зазвичай рівні, ви хочете порівняти їх останніми, і компілятор не знає, які члени, швидше за все, рівні.

Розглянемо цей приклад, де verboseDescriptionдовга струна обрана із відносно невеликого набору можливих погодних описів.

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

(Звичайно, компілятор мав би право знехтувати порядком порівнянь, якщо визнає, що вони не мають побічних ефектів, але, мабуть, він все-таки відіб'є свою чергу з вихідного коду там, де не має кращої інформації.)


Але ніхто не заважає Вам написати оптимізуючу порівняння, визначену користувачем, якщо Ви знайдете проблеми з продуктивністю. На моєму досвіді, це було б малою мірою випадків.
Пітер -

1

Просто так, що відповіді на це питання залишаються повними, як проходить час: оскільки C ++ 20 його можна автоматично генерувати за допомогою команди auto operator<=>(const foo&) const = default;

Він генерує всіх операторів: ==,! =, <, <=,> І> =, див. Https://en.cppreference.com/w/cpp/language/default_comparisons для детальної інформації.

Через зовнішній вигляд оператора <=>його називають оператором космічного корабля. Також дивіться, для чого нам потрібен оператор космічного корабля <=> в C ++? .

EDIT: Крім того, в C ++ 11 досить акуратний замінник , який доступний з std::tieсм https://en.cppreference.com/w/cpp/utility/tuple/tie для повного прикладу коду з bool operator<(…). Цікава частина, змінена для роботи з ==:

#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 працює з усіма операторами порівняння і повністю оптимізований компілятором.


-1

Я згоден, для класів типу POD тоді компілятор міг би зробити це за вас. Однак те, що ви можете вважати простим, компілятор може помилитися. Тож краще дозволити програмісту це зробити.

Одного разу у мене був випадок POD, коли два поля були унікальними - тому порівняння ніколи не вважатиметься правдивим. Однак порівняння, яке мені потрібно було, коли-небудь порівнювало на корисну навантаження - те, що компілятор ніколи не зрозуміє і не міг зрозуміти, що це самостійно.

Крім того - писати їм не потрібно багато часу ?!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.