Почему компиляторы 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

Компилятор не знает, нужно ли вам сравнение указателей или глубокое (внутреннее) сравнение.

Безопаснее просто не реализовывать это и позволить программисту делать это самостоятельно. Тогда они могут сделать все предположения, которые им нравятся.


293
Эта проблема не останавливает его от создания копии ctor, где это довольно вредно.
MSalters

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

46
-1. Конечно, вы хотите глубокое сравнение, если программисту нужно сравнение указателей, он написал бы (& f1 == & f2)
Виктор Сехр

62
Виктор, предлагаю переосмыслить свой ответ. Если класс Foo содержит Bar *, то как компилятор узнает, хочет ли Foo :: operator == сравнить адрес Bar * или содержимое Bar?
Марк Ингрэм

46
@Mark: если он содержит указатель, сравнение значений указателя является разумным - если оно содержит значение, сравнение значений является разумным. В исключительных случаях программист может переопределить. Это так же, как язык реализует сравнение между целыми и указателями на целые.
Имон Нербонн

317

Аргумент, что если компилятор может предоставить конструктор копирования по умолчанию, он должен быть в состоянии обеспечить аналогичное значение по умолчанию, operator==()имеет определенный смысл. Я думаю, что причина решения не предоставлять сгенерированный компилятором по умолчанию для этого оператора может быть угадана из того, что сказал Страуструп о конструкторе копирования по умолчанию в «Проектировании и развитии C ++» (Раздел 11.4.1 - Управление копированием) :

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

Таким образом, вместо «почему в C ++ нет значения по умолчанию operator==()?», Вопрос должен был звучать так: «Почему в C ++ есть конструктор присваивания и копирования по умолчанию?», Ответ на который заключается в том, что эти элементы неохотно включались в Stroustrup для обратной совместимости с C (вероятно, причина большинства бородавок C ++, но также, вероятно, основная причина популярности C ++).

Для моих собственных целей в моей IDE сниппет, который я использую для новых классов, содержит объявления для частного оператора присваивания и конструктора копирования, так что при создании нового класса я не получаю операции присваивания и копирования по умолчанию - мне нужно явно удалить объявление из этих операций из private:раздела, если я хочу, чтобы компилятор мог генерировать их для меня.


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

9
«Тем не менее, C ++ унаследовал свое назначение по умолчанию и скопировал конструкторы из C». Это не означает, что вы должны создавать ВСЕ типы C ++ таким образом. Они должны были просто ограничить это простыми старыми POD, только теми типами, которые уже есть в C, не более.
thesaint

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
Модифицированное предложение, которое допускает то же самое (кроме краткой формы), будет в C ++ 20, хотя :)
Rakete1111

Так что в основном вы должны указать = default, для вещи, которая не создана по умолчанию, верно? Для меня это звучит как оксюморон («явное значение по умолчанию»).
артины

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

44

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

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


39

Он ответил, что C ++ не сделал ==, потому что C не сделал, и вот почему C предоставляет только default =, но не == на первом месте. C хотел, чтобы все было просто: C реализовано = memcpy; однако == не может быть реализован memcmp из-за заполнения. Поскольку padding не инициализируется, memcmp говорит, что они разные, хотя они одинаковые. Та же проблема существует для пустого класса: memcmp говорит, что они разные, потому что размер пустых классов не равен нулю. Из вышесказанного видно, что реализация == более сложна, чем реализация = в C. Некоторые примеры кода, касающиеся этого. Ваша поправка приветствуется, если я ошибаюсь.


6
C ++ не использует memcpy для operator=- это будет работать только для типов POD, но C ++ также предоставляет значение operator=по умолчанию для не POD-типов.
Flexo

2
Да, C ++ реализован = более изощренным способом. Кажется, C только что реализовал = с простой memcpy.
Крыло Рио

Содержание этого ответа следует соединить с ответом Михаила. Он исправляет вопрос, а затем отвечает на него.
Sgene9

27

В этом видео Алексей Степанов, создатель STL, решает этот вопрос примерно в 13:00. Подводя итог, наблюдая за развитием C ++, он утверждает, что:

  • К сожалению, == и! = Не объявляются неявно (и Бьярне соглашается с ним). В правильном языке эти вещи должны быть готовы для вас (он далее рекомендует, что вы не сможете определить ! = , Который нарушает семантику == )
  • Причина, по которой дело обстоит так, имеет свои корни (как и многие проблемы C ++) в C. Там оператор присваивания неявно определяется с побитовым присваиванием, но это не сработает для == . Более подробное объяснение можно найти в этой статье Бьярна Страуструпа.
  • В последующем вопросе « Почему тогда не использовалось сравнение членов по элементам», он говорит удивительную вещь : C был своего рода доморощенным языком, и парень, реализующий эти вещи для Ричи, сказал ему, что он считает, что это трудно реализовать!

Затем он говорит, что в (далеком) будущем == и ! = Будут сгенерированы неявно.


2
кажется, что это отдаленное будущее не будет 2017, ни 18, ни 19, ну, вы поймаете мой дрейф ...
UmNyobe

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<только.
ил

О возвращаемом типе auto: для этого случая мы можем всегда предполагать, что это будет std::strong_orderingиз #include <compare>?
Кевинарпе

1
@kevinarpe Тип возвращаемого значения std::common_comparison_category_t, который для этого класса становится ordering по умолчанию ( std::strong_ordering).
ил

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 :: operator .

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

Вы также можете генерировать «+» из «+ =», - из «- =» и т. Д. (См. Полный список здесь )


Я не получил дефолт !=после написания ==оператора. Или я сделал, но ему не хватало const. Пришлось написать самому тоже и все было хорошо.
Джон

Вы можете играть с постоянством, чтобы достичь необходимых результатов. Без кода сложно сказать, что с ним не так.
sergtk

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

10

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


3
Я думал, что только «специальные функции-члены» (конструктор по умолчанию, конструктор копирования, оператор присваивания и деструктор) могут быть явно дефолтными. Они распространили это на некоторых других операторов?
Майкл Берр

4
Конструктор Move также может быть установлен по умолчанию, но я не думаю, что это относится к operator==. Что жаль.
Павел Минаев

5

Концептуально не легко определить равенство. Даже для данных POD можно утверждать, что даже если поля одинаковы, но это другой объект (по другому адресу), он не обязательно равен. Это на самом деле зависит от использования оператора. К сожалению, ваш компилятор не экстрасенс и не может вывести это.

Помимо этого, стандартные функции являются отличным способом выстрелить себе в ногу. Описанные вами значения по умолчанию в основном сохраняют совместимость со структурами POD. Однако они приводят к более чем достаточному хаосу, когда разработчики забывают о них или семантике реализаций по умолчанию.


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

1
@mgiuca: я вижу значительную полезность для универсального отношения эквивалентности, которое позволило бы любому типу, который ведет себя как значение, использоваться в качестве ключа в словаре или подобной коллекции. Однако такие коллекции не могут вести себя бесполезно без отношения эквивалентности с гарантированной рефлексивностью. ИМХО, лучшим решением было бы определить новый оператор, который могли бы разумно реализовать все встроенные типы, и определить некоторые новые типы указателей, которые были бы похожи на существующие, за исключением того, что некоторые определяли бы равенство как ссылочную эквивалентность, в то время как другие связывали бы с целью оператор эквивалентности.
Суперкат

1
@supercat По аналогии с +оператором можно привести почти такой же аргумент, что он неассоциативен для чисел с плавающей точкой; то есть (x + y) + z! = x + (y + z), из-за способа округления FP. (Возможно, это гораздо более серьезная проблема, чем то, ==что она имеет место для обычных числовых значений.) Вы можете предложить добавить новый оператор сложения, который работает для всех числовых типов (даже целых) и почти точно такой же, как +и ассоциативный ( как - то). Но тогда вы добавите к языку раздувание и путаницу, не помогая такому количеству людей.
mgiuca

1
@mgiuca: Наличие вещей, которые очень похожи, за исключением крайних случаев, часто чрезвычайно полезно, а ошибочные попытки избежать таких вещей приводят к значительной ненужной сложности. Если клиентскому коду иногда нужно обрабатывать граничные случаи одним способом, а иногда нужно обрабатывать их другим способом, наличие метода для каждого стиля обработки устраняет большую часть кода обработки крайних случаев в клиенте. Что касается вашей аналогии, то нет способа определить операцию со значениями с плавающей запятой фиксированного размера для получения переходных результатов во всех случаях (хотя некоторые языки 1980-х имели лучшую семантику ...
суперкат

1
... чем сегодня в этом отношении) и, следовательно, тот факт, что они не делают невозможного, не должен быть сюрпризом. Однако нет принципиальных препятствий для реализации отношения эквивалентности, которое было бы универсально применимо к любому типу значения, которое можно скопировать.
суперкат

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 ++? ,

РЕДАКТИРОВАТЬ: также в 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.