Я большой поклонник того, чтобы компилятор делал за вас как можно больше работы. При написании простого класса компилятор может «бесплатно» предоставить вам следующее:
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
{ }
Есть ли для этого веская причина? Почему выполнение сравнения по каждому члену может быть проблемой? Очевидно, что если класс выделяет память, вы должны быть осторожны, но для простого класса компилятор мог бы сделать это за вас?
В одном из своих недавних выступлений Алекс Степанов указал, что было ошибкой не иметь автоматического назначения по умолчанию ==, точно так же, как есть автоматическое назначение по умолчанию (=) при определенных условиях. (Аргумент об указателях непоследователен, потому что логика применима как к =, так и к ==, а не только ко второму).
@becko, это одна из первых в серии «Эффективное программирование с компонентами» или «Программирование разговоров» на A9, доступных на Youtube.
См. Этот ответ для информации о C++ 20: stackoverflow.com/a/50345359





Я согласен, для классов типа POD компилятор мог бы сделать это за вас. Однако то, что вы считаете простым, компилятор может ошибаться. Так что лучше пусть это сделает программист.
Однажды у меня был случай POD, в котором два поля были уникальными, поэтому сравнение никогда не считалось верным. Однако сравнение, которое мне нужно, когда-либо сравнивалось только по полезной нагрузке - то, что компилятор никогда не поймет или когда-либо сможет выяснить самостоятельно.
К тому же - они пишут недолго, не так ли ?!
Компилятор не узнает, нужно ли вам сравнение указателей или глубокое (внутреннее) сравнение.
Безопаснее просто не реализовывать это и позволить программисту сделать это самим. Затем они могут делать любые предположения, которые им нравятся.
Эта проблема не мешает ему генерировать копию ctor, где это довольно вредно.
Конструкторы копирования используются в совершенно другом контексте, чем операторы сравнения. И, IMHO, его контекст понятен в том, что он делает.
Существует проблема совместимости с C: C89 создает код для структур, который имитирует присваивание C++ (и, возможно, конструкторы копирования ... я должен проверить). Таким образом, обычно C++ генерирует похожие коды.
Конструкторы копирования (и operator=) обычно работают в том же контексте, что и операторы сравнения, то есть ожидается, что после выполнения a = ba == b верен. Компилятору определенно имеет смысл предоставить operator== по умолчанию, используя ту же семантику агрегированного значения, что и для operator=. Я подозреваю, что paercebal действительно прав в том, что operator= (и copy ctor) предоставляются исключительно для совместимости с C, и они не хотели ухудшать ситуацию.
-1. Конечно, вам нужно глубокое сравнение, если бы программист хотел сравнение указателя, он бы написал (& f1 == & f2)
Виктор, предлагаю переосмыслить свой ответ. Если класс Foo содержит Bar *, то как компилятор узнает, хочет ли Foo :: operator == сравнить адрес Bar * или содержимое Bar?
@Mark: если он содержит указатель, сравнение значений указателя является разумным - если он содержит значение, сравнение значений разумно. В исключительных случаях программист может отменить. Это похоже на то, как в языке реализовано сравнение между целыми числами и указателями на целые числа.
-1, как указывалось другими, это непоследовательный аргумент по сравнению с operator =
Ни одна из указанных причин не является действительной. Если бы в стандарте C++ говорилось, что компиляторы должны предоставлять по умолчанию оператор ==, сравнивающий каждое значение поля, это было бы не самым безумным. И, черт возьми, если нам нужна защита от указателей, просто выдайте предупреждение или ошибку в таких случаях.
@MarkIngram: ваше замечание не имеет смысла. компилятор, реализованный оператором ==, должен просто вызывать операторы == для всех членов, если они доступны. так, например, clone_ptr не нарушит глубокую цепочку. и std :: vector тоже. однако указатели и интеллектуальные указатели будут сравниваться неглубоко.
Компилятору не нужно в этом разбираться. Один из двух вариантов может быть указан в качестве стандарта, тогда поставщики компилятора просто должны будут следовать этому стандарту. Очевидным выбором, конечно же, было бы неглубокое сравнение указателей и сравнение значений для членов, точно так, как описывает v.oddou.
Почему бы не применить ту же логику и для оператора присваивания? Возникла проблема несогласованности.
@JollyRoger, не совсем, оператор присваивания может просто назначать каждую переменную-член, двусмысленности нет.
Я не понимаю, насколько сложно сравнивать ценность каждого члена по отдельности. Если бы пользователь хотел сравнить адреса каждого объекта, он бы реализовал &A == &B и четко об этом сообщил. Что касается глубоких сравнений, таких как strcmp, они не вызывают проблемы, потому что C++ довольно четко понимает, что указатели являются целочисленными значениями (адресами), которые можно нормально сравнивать (==) с другим указателем.
Это несоответствие сохраняется и в стандартном комитете. В C++ 20 они изменили свое мнение. Теперь вы можете запросить оператор сравнения по умолчанию (при условии строгого порядка).
C++ 0x имеет предлагал функции по умолчанию, так что вы могли бы сказать default operator==;
Мы узнали, что это помогает сделать эти вещи явными.
Конструктор перемещения также может быть установлен по умолчанию, но я не думаю, что это относится к operator==. Какая жалость.
Концептуально определить равенство непросто. Даже для данных POD можно утверждать, что даже если поля одинаковы, но это другой объект (по другому адресу), они не обязательно равны. На самом деле это зависит от использования оператора. К сожалению, ваш компилятор не обладает экстрасенсорными способностями и не может этого сделать.
Кроме того, стандартные функции - отличный способ выстрелить себе в ногу. Описанные вами значения по умолчанию в основном предназначены для обеспечения совместимости со структурами POD. Однако они вызывают более чем достаточный хаос, когда разработчики забывают о них или о семантике реализаций по умолчанию.
Для структур POD нет двусмысленности - они должны вести себя точно так же, как и любой другой тип POD, а именно равенство значений (а не ссылочное равенство). Один int, созданный посредством копирования ctor из другого, равен тому, из которого он был создан; единственное, что логично сделать для struct с двумя полями int, - это работать точно так же.
Хорошо, но что, если структура POD имеет значение, релевантность которого зависит от другого значения. В случае, если значение не имеет значения, побитовое равенство больше не выполняется. Равенство действительно может варьироваться от одного и того же объекта (только с указателями / ссылками) до чего-либо, что несколько экономит (возможно, подкласса)
Это правда, но в C++ есть четкое определение «значения» (это все, что копируется в конструкторе копирования по умолчанию). Равенство по умолчанию, если оно есть, определенно будет работать таким же образом: оно просто вызовет operator= для каждого поля. Это гарантирует, что любые значения POD сравниваются как значения, указатели сравниваются по ссылке, а объекты с переопределенными методами operator= сравниваются так, как задумано разработчиком класса. Я бы сказал, что концептуально является легко определить равенство. Они не сделали этого по другим причинам.
@PavelMinaev Даже для структур POD это зависит от того, что они представляют, и являются ли они одинаковыми. Следует ли определять равенство как идентичность, эквивалентность или другой вариант. Я согласен с вами, что для многих структур POD правильным ответом является эквивалентность (одинаковые значения данных).
@PavelMinaev: Все варианты использования универсальных средств тестирования эквивалентности, которые я могу придумать, требуют, чтобы каждый объект сравнивался эквивалентным самому себе. К сожалению, IEEE-754 запрещает операторам == и != проявлять такое поведение.
@mgiuca: Учитывая struct {double v;} a,b; a.v=0.0/0.0; b=a;, что должен сообщить a==b? Если бы не нарушенное поведение == с типами с плавающей запятой, тогда a.v==b.v был бы истинным, а a==b не представлял бы проблемы. Тем не менее, хотя я вижу большую ценность в наличии стандартных средств запроса объектов на проверку эквивалентности, такая полезность будет зависеть от договорного требования, согласно которому тест ведет себя как отношение эквивалентности, чего оператор == не может сделать. со всеми типами.
@supercat Почему вы считаете, что автоматический оператор == должен быть обязательным для удовлетворения рефлексивности? Я согласен, что это было бы идеально, но, учитывая, что в C++ уже есть операторы ==, которые не являются рефлексивными (как вы указываете), автоматически сгенерированный оператор == для составного типа может быть настолько хорош, насколько хорош операторы == его полей. Это не умаляет полезности автоматической генерации оператора == по умолчанию, который говорит, что «мы равны, если все наши поля равны согласно ==».
@mgiuca: Я вижу значительную полезность универсального отношения эквивалентности, которое позволило бы использовать любой тип, который ведет себя как значение, в качестве ключа в словаре или подобной коллекции. Однако такие коллекции не могут работать с пользой без отношения гарантированно-рефлексивной эквивалентности. IMHO, лучшим решением было бы определить новый оператор, который все встроенные типы могли бы разумно реализовать, и определить некоторые новые типы указателей, которые были похожи на существующие, за исключением того, что некоторые определяли бы равенство как ссылочную эквивалентность, в то время как другие связывались бы с целевым оператор эквивалентности.
@supercat: Это действительно просто аргумент для реализаций ==, чтобы они подчинялись правилам отношений эквивалентности. К сожалению, этот корабль уже отплыл, и я не думаю, что введение нового оператора, который почти точно такой же, как ==, за исключением нескольких угловых случаев, действительно так полезно. Насколько мне известно, встроенные типы все подчиняются законам эквивалентности, отличным от float и double. Конечно, определяемые пользователем типы могут определять == и нарушать эти законы, но они тоже могут это сделать с вашим новым оператором. Ничто из этого не влияет на то, полезно ли иметь автоматический == для пользовательских типов или нет.
@mgiuca: Есть много ситуаций, когда необходимо проверить эквивалентность чисел с плавающей запятой. Если программисты, желающие проверить эквивалентность fp, должны написать что-то вроде bool equals(double x, double y) { return (x==y) || (x!=x) && (y!=y); }, такое тестирование будет неэффективным даже на оборудовании, которое позволяет проводить прямое тестирование эквивалентности. Добавление средства проверки эквивалентности, которое будет работать для типов fp, но также может быть реализовано другими типами, которые обещают, что оно представляет отношение эквивалентности, казалось бы лучше, чем добавление одного только для типов fp.
@supercat Извините за медленный ответ. Да, я согласен с тем, что было бы полезно иметь надлежащий эквивалент для FP. Скорее, корень проблемы, о которой вы говорите, заключается не в том, что оператор == нарушен, а в том, что нарушено равенство для FP. == практически во всех смыслах и целях действительно представляет собой класс эквивалентности, и поэтому автоматическое расширение его до структур имеет смысл. У него просто есть один редкий крайний случай, когда он сломан. Добавление нового оператора только запутает ситуацию и практически не решит никаких проблем. («Почему есть 2 оператора равенства ?!»)
@supercat По аналогии, вы можете привести почти тот же аргумент для оператора +, поскольку он неассоциативен для чисел с плавающей запятой; то есть (x + y) + z! = x + (y + z) из-за способа округления FP. (Возможно, это гораздо более серьезная проблема, чем ==, потому что это верно для обычных числовых значений.) Вы можете предложить добавить новый оператор сложения, который работает для всех числовых типов (даже int) и почти такой же, как +, но он ассоциативный (как-то). Но тогда вы добавили бы в язык раздувания и путаницы, не помогая на самом деле такому количеству людей.
@mgiuca: Часто очень сильно полезно иметь вещи, которые очень похожи, за исключением крайних случаев, и ошибочные попытки избежать таких вещей приводят к ненужным сложностям. Если клиентскому коду иногда требуется, чтобы крайние случаи обрабатывались одним способом, а иногда - другим, наличие метода для каждого стиля обработки избавит клиента от большого количества кода обработки крайних случаев. Что касается вашей аналогии, нет способа определить операцию со значениями с плавающей запятой фиксированного размера для получения транзитивных результатов во всех случаях (хотя некоторые языки 1980-х годов имели лучшую семантику ...
... чем сегодня в этом отношении), и поэтому тот факт, что они не делают невозможного, не должен быть сюрпризом. Однако нет фундаментальных препятствий для реализации отношения эквивалентности, которое было бы универсально применимо к любому типу значения, которое можно скопировать.
Невозможно определить == по умолчанию, но вы можете определить != по умолчанию через ==, который вы обычно должны определить сами.
Для этого вам необходимо сделать следующее:
#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, лучше, потому что он определяет использование оператора только для нужного вам класса, а не для всех классов в области видимости.
Вы также можете генерировать «+» из «+ =», - из «- =» и т. д. (См. Полный список здесь)
Есть причина, по которой rel_ops устарел в C++ 20: потому что это не работает, по крайней мере, не везде и, конечно, не всегда. Нет надежного способа заставить sort_decreasing() скомпилировать. С другой стороны, Boost.Operators работает и всегда работал.
Аргумент о том, что, если компилятор может предоставить конструктор копирования по умолчанию, он должен иметь возможность предоставить аналогичный по умолчанию operator==(), имеет определенный смысл. Я думаю, что причина решения не предоставлять для этого оператора значение по умолчанию, созданное компилятором, можно догадаться по тому, что Страуструп сказал о конструкторе копирования по умолчанию в «Проектировании и развитии C++» (Раздел 11.4.1 - Контроль копирования) :
I personally consider it unfortunate that copy operations are defined by default and I prohibit copying of objects of many of my classes. However, C++ inherited its default assignment and copy constructors from C, and they are frequently used.
Поэтому вместо «почему в C++ нет operator==() по умолчанию?» Должен был быть вопрос «почему в C++ есть конструктор присваивания и копирования по умолчанию?», С ответом, что эти элементы были включены Stroustrup неохотно для обратной совместимости с C (вероятно, причина большинства недостатков C++, но также, вероятно, основная причина популярности C++).
Для моих собственных целей в моей среде IDE фрагмент, который я использую для новых классов, содержит объявления для частного оператора присваивания и конструктора копирования, поэтому, когда я создаю новый класс, я не получаю операций присваивания и копирования по умолчанию - я должен явно удалить объявление этих операций из раздела private:, если я хочу, чтобы компилятор мог сгенерировать их для меня.
Хороший ответ. Я просто хотел бы отметить, что в C++ 11 вместо того, чтобы делать оператор присваивания и конструктор копирования частными, вы можете полностью удалить их следующим образом: Foo(const Foo&) = delete; // no copy constructor и Foo& Foo=(const Foo&) = delete; // no assignment operator
«Однако C++ унаследовал свои конструкторы присваивания и копирования по умолчанию от C». Это не означает, почему вы должны создавать ВСЕ типы C++ таким образом. Они должны были просто ограничить это простыми старыми POD, только типами, которые уже есть в C, не более.
Я, конечно, могу понять, почему C++ унаследовал это поведение от struct, но я действительно хочу, чтобы он позволял class вести себя по-другому (и разумно). В процессе это также дало бы более значимую разницу между struct и class, помимо доступа по умолчанию.
ИМХО, "веской" причины нет. Причина, по которой так много людей согласны с этим дизайнерским решением, заключается в том, что они не научились овладевать мощью семантики, основанной на значениях. Людям нужно написать много настраиваемых конструкторов копирования, операторов сравнения и деструкторов, потому что они используют необработанные указатели в своей реализации.
При использовании соответствующих интеллектуальных указателей (например, std :: shared_ptr) конструктор копирования по умолчанию обычно подходит, и очевидная реализация гипотетического оператора сравнения по умолчанию будет такой же хорошей.
На него ответили, что C++ не сделал ==, потому что C этого не сделал, и вот почему C предоставляет только default =, но не == в первую очередь. C хотел, чтобы все было просто: C реализован = с помощью memcpy; однако == не может быть реализован memcmp из-за заполнения. Поскольку заполнение не инициализируется, memcmp сообщает, что они разные, даже если они одинаковы. Та же проблема существует для пустого класса: memcmp говорит, что они разные, потому что размер пустых классов не равен нулю. Из вышесказанного видно, что реализация == более сложна, чем реализация = в C. Некоторый код пример относительно этого. Ваше исправление приветствуется, если я ошибаюсь.
C++ не использует memcpy для operator= - это будет работать только для типов POD, но C++ предоставляет operator= по умолчанию и для типов, отличных от POD.
Да, C++ реализовал = более изощренным способом. Кажется, C просто реализовал = с простым memcpy.
В этом видео Алексей Степанов, создатель STL, отвечает на этот вопрос примерно в 13:00. Подводя итог, наблюдая за эволюцией C++, он утверждает, что:
Затем он говорит, что в (отдаленном) будущем == и знак равно будут генерироваться неявно.
Даже в 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<=>.
@ dcmm88 К сожалению, он не будет доступен в C++ 17. Я обновил ответ.
Однако модифицированное предложение, которое допускает то же самое (кроме краткой формы), будет в C++ 20 :)
@artin Это имеет смысл, поскольку добавление новых функций в язык не должно нарушать существующую реализацию. Добавление новых стандартов библиотеки или новых возможностей компилятора - это одно. Добавление новых функций-членов туда, где их раньше не было, - это совсем другая история. Чтобы обезопасить свой проект от ошибок, потребуется гораздо больше усилий. Я лично предпочел бы, чтобы флаг компилятора переключался между явным и неявным значением по умолчанию. Вы строите проект из более старого стандарта C++, используйте явное значение по умолчанию по флагу компилятора. Вы уже обновили компилятор, поэтому вам следует правильно его настроить. Для новых проектов сделайте это неявным.
Is there a good reason for this? Why would performing a member-by-member comparison be a problem?
Функционально это может не быть проблемой, но с точки зрения производительности сравнение отдельных элементов по умолчанию может быть более неоптимальным, чем присвоение / копирование отдельных элементов по умолчанию. В отличие от порядка присваивания, порядок сравнения влияет на производительность, поскольку первый неравный член подразумевает, что остальные могут быть пропущены. Поэтому, если есть некоторые члены, которые обычно равны, вы хотите сравнить их последними, и компилятор не знает, какие члены с большей вероятностью будут равны.
Рассмотрим этот пример, где 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
}
}
(Конечно, компилятор будет иметь право игнорировать порядок сравнений, если он признает, что у них нет побочных эффектов, но, по-видимому, он все равно будет брать свою очередь из исходного кода, где у него нет лучшей собственной информации.)
Но никто не мешает вам написать оптимизирующее сравнение, определяемое пользователем, если вы обнаружите проблему с производительностью. По моему опыту, это будет незначительное количество случаев.
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<=>
Я удивлен, что они использовали Point в качестве примера для операции заказ, поскольку не существует разумного способа по умолчанию упорядочить две точки с координатами x и y ...
@pipe Если вам все равно, в каком порядке расположены элементы, имеет смысл использовать оператор по умолчанию. Например, вы можете использовать std::set, чтобы убедиться, что все точки уникальны, а std::set использует только operator<.
О типе возврата auto: для этот случай всегда можно предположить, что это будет std::strong_ordering от #include <compare>?
@kevinarpe Тип возврата - std::common_comparison_category_t, который для этого класса становится порядком по умолчанию (std::strong_ordering).
Просто чтобы ответы на этот вопрос оставались полными с течением времени: начиная с 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 работает со всеми операторами сравнения и полностью оптимизируется компилятором.
Конечно, деструктор предоставляется бесплатно.