Comparison operators
Выражения операторов двустороннего сравнения имеют форму.
lhs < rhs | (1) |
lhs > rhs | (2) |
lhs <= rhs | (3) |
lhs >= rhs | (4) |
lhs == rhs | (5) |
lhs != rhs | (6) |
Во всех случаях,для встроенных операторов,lhs и rhs должны иметь либо то,либо другое.
- арифметика или тип перечисления (см.арифметические операторы сравнения ниже)
- тип указателя (см.ниже операторы сравнения указателей)
после применения стандартных преобразований lvalue-to-rvalue , массива в указатель и функции в указатель . Сравнение устарело, если оба операнда имеют тип массива до применения этих преобразований (начиная с C++20).
В любом случае, результатом является предварительное значение bool .
операторы арифметического сравнения
Если операнды имеют арифметический тип или тип перечисления (scoped или unscoped), обычные арифметические преобразования выполняются для обоих операндов в соответствии с правилами для арифметических операторов . Значения сравниваются после преобразования:
Example
Операторы сравнения указателей
Операторы сравнения можно использовать для сравнения двух указателей.
Только операторы равенства ( operator== и operator!= ) можно использовать для сравнения следующих пар указателей:
- two pointers-to-members
- константа нулевого указателя с указателем или указателем на член
- значение std::nullptr_t с константой нулевого указателя (которая также может быть значением std::nullptr_t )
Во- первых, к обоим операндам применяются преобразования указателей (преобразования указателя в член, если аргументы являются указателями на члены), преобразования указателя функции (начиная с C++17) и преобразования квалификации применяются к обоим операндам для получения комбинированный указатель , следующим образом.
1) Если оба операнда являются константами нулевого указателя, тип составного указателя является std::nullptr_t | (since C++11) |
- указатель на cv1 void , and
- указатель на cv2 T , где T — тип объекта или void ,
- P1 , указатель на (возможно, cv-квалифицированный) T1 , и
- P2 , указатель на (возможно, cv-квалифицированный) T2 ,
- MP1 , указатель на элемент T1 типа (возможно, cv-квалифицированный) U1 , и
- MP2 , указатель на элемент T2 типа (возможно, cv-квалифицированный) U2 ,
В приведенном выше определении, cv-combined Тип двух указателей P1 и P2 — это тип P3 , который имеет то же количество уровней и тип на каждом уровне, что и P1 , за исключением того, что cv-квалификации на каждом уровне устанавливаются следующим образом:
Например, типом составного указателя void* и const int* является const void* . Тип составного указателя int** и const int** — const int* const* . Обратите внимание, что до решения CWG1512 нельзя было сравнивать int int** и const int** .
В дополнение к вышесказанному,составной тип указателя между указателем на функцию и указателем на неисключительную функцию (до тех пор,пока тип функции совпадает)является указателем на функцию.
Обратите внимание, что это означает, что любой указатель можно сравнить с void* .
Результат сравнения двух указателей на объекты (после преобразования)определяется следующим образом:
Результат сравнения равенства двух указателей (после преобразования)определяется следующим образом:
Результат сравнения двух указателей с членами (после преобразования)определяется следующим образом:
Если указатель p compare equal чтобы указатель q , p<=q и p>=q как выход true и p<q и p>q и выход false .
Если указатель p compares greater чем указатель q , то p>=q , p>q , q<=p и q<p все дают true , а p<=q , p<q , q>=p и q>p все дают false .
Если два указателя не указаны для сравнения больше или равно,результат сравнения не определен.Неопределенный результат может быть недетерминированным и не обязательно должен быть последовательным даже для нескольких оценок одного и того же выражения с одними и теми же операндами при одном и том же выполнении программы:
При разрешении перегрузки для определенных пользователем операторов для каждой пары повышенных арифметических типов L и R , включая типы перечисления, следующие сигнатуры функций участвуют в разрешении перегрузки:
Для каждого типа P , который является либо указателем на объект, либо указателем на функцию, следующие сигнатуры функций участвуют в разрешении перегрузки:
Для каждого типа MP , который является указателем на объект-член или указателем на функцию-член или std::nullptr_t , следующие сигнатуры функций участвуют в разрешении перегрузки:
Example
Notes
Поскольку эти операторы группируются слева направо, выражение a<b<c анализируется (a<b)<c , а не a<(b<c) или (a<b)&&(b<c) .
Общим требованием для пользовательского operator< является строгое слабое упорядочение . В частности, этого требуют стандартные алгоритмы и контейнеры, работающие с типами Compare : std::sort , std::max_element , std::map и т. д.
Хотя результаты сравнения указателей случайного происхождения (например, не все указывают на элементы одного и того же массива) не определены, многие реализации обеспечивают строгое полное упорядочение указателей, например, если они реализованы как адреса в непрерывном виртуальном адресном пространстве. Те реализации, которые не (например, где не все биты указателя являются частью адреса памяти и должны игнорироваться для сравнения, или требуется дополнительное вычисление, или иначе указатель и целое число не являются отношением 1 к 1), обеспечивают специализация std::less для указателей с такой гарантией. Это позволяет использовать все указатели случайного происхождения в качестве ключей в стандартных ассоциативных контейнерах, таких как std::set или std::map .
Для типов, которые являются EqualityComparable и LessThanComparable , стандартная библиотека C ++ делает различие между equality , которое является значением выражения a == b и equivalence , которое является значением выражения !(a < b) && !(b < a) .
Сравнение между указателями и константами пустых указателей было удалено решением проблемы CWG 583 , включенной в N3624 .
Урок №42. Операторы сравнения
Вы уже могли их видеть в коде. Они довольно простые. Каждый из этих операторов вычисляется в логическое значение true (1) или false (0).
Вот несколько примеров использования этих операторов на практике:
Результат выполнения программы:
Enter an integer: 4
Enter another integer: 5
4 does not equal 5
4 is less than 5
4 is less than or equal to 5
Сравнение чисел типа с плавающей точкой
Сравнение значений типа с плавающей точкой с помощью любого из этих операторов — дело опасное. Почему? Из-за тех самых небольших ошибок округления, которые могут привести к неожиданным результатам. Например:
В вышеприведенной программе d1 = 0.0100000000000005116 , а d2 = 0.0099999999999997868 . Значения обоих этих чисел очень близки к 0.1 , но d1 больше d2 . Они не равны.
Иногда сравнение чисел типа с плавающей точкой бывает неизбежным. В таком случае следует использовать операторы > , < , >= и <= только если значения этих чисел сильно отличаются друг от друга. А вот если два операнда почти равны, то результат уже может быть неожиданный. В вышеприведенном примере последствия неправильного результата незначительны, а вот с оператором равенства дела обстоят хуже, так как даже при самой маленькой неточности результат сразу меняется на противоположный ожидаемому. Не рекомендуется использовать операторы == или != с числами типа с плавающей точкой. Вместо них следует использовать функцию, которая вычисляет, насколько эквивалентны эти два значения. Если разницей между ними можно пренебречь, то мы считаем их равными. Значение разницы между числами, которой можно пренебречь, называется эпсилоном. Оно, обычно, небольшое (например, 0.0000001 ).
Очень часто начинающие разработчики пытаются писать свои собственные функции определения равенства чисел:
Примечание: Функция fabs() — это функция из заголовочного файла cmath, которая возвращает абсолютное значение (модуль) параметра. fabs(а − b) возвращает положительное число как разницу между а и b .
Функция isAlmostEqual() из примера, приведенного выше, сравнивает разницу (a − b) и эпсилон, вычисляя, таким образом, можно ли считать эти числа равными. Если разница между а и b очень мала, то функция возвращает true.
Хоть это и рабочий вариант, но он не идеален. Эпсилон 0.00001 подходит для чисел около 1.0, но будет слишком большим для чисел типа 0.0000001 и слишком малым для чисел типа 10000. Это означает, что каждый раз при вызове функции нам нужно будет выбирать наиболее соответствующий входным данным функции эпсилон.
Дональд Кнут, известный учёный, предложил следующий способ в своей книге «Искусство программирования, том 2: Получисленные алгоритмы» (1968):
Здесь, вместо использования эпсилона как абсолютного числа, мы используем его как умножитель, чтобы подстроиться под входные данные.
Рассмотрим детально, как работает функция approximatelyEqual(). Слева от оператора <= абсолютное значение (а − b) сообщает нам разницу между а и b (положительное число). Справа от <= нам нужно вычислить эпсилон, т.е. наибольшее значение разности чисел, которое мы готовы принять. Для этого алгоритм выбирает большее из чисел а и b (как приблизительный показатель общей величины чисел), а затем умножает его на эпсилон. В этой функции эпсилон представляет собой процентное соотношение. Например, если разница между числами а и b находится в пределах 1% (больше или меньше), то мы вводим эпсилон 1% (1% = 1/100 = 0.01). Его значение можно легко регулировать, в зависимости от обстоятельств (например, 0.01% = эпсилон 0.0001). Чтобы сделать неравенство ( != ) вместо равенства — просто вызовите эту функцию, используя логический оператор НЕ ( ! ), чтобы перевернуть результат:
Но и функция approximatelyEqual() тоже не идеальна, особенно, когда дело доходит до чисел, близких к нулю:
Возможно, вы удивитесь, но результат:
Второй вызов не сработал так, как ожидалось. Математика просто ломается, когда дело доходит до нулей.
Но и этого можно избежать, используя как абсолютный эпсилон (то, что мы делали в первом способе), так и относительный (способ Кнута) вместе:
Здесь мы добавили новый параметр — absEpsilon . Сначала мы сравниваем а и b с absEpsilon , который должен быть задан как очень маленькое число (например, 1e-12 ). Таким образом, мы решаем случаи, когда а и b — нулевые значения или близки к нулю. Если это не так, то мы возвращаемся к алгоритму Кнута.
С удачно подобранным absEpsilon , функция approximatelyEqualAbsRel() обрабатывает близкие к нулю и нулевые значения корректно.
Сравнение чисел типа с плавающей точкой — сложная тема, и нет одного идеального алгоритма, который подойдет в любой ситуации. Однако для большинства случаев, с которыми вы будете сталкиваться, функции approximatelyEqualAbsRel() должно быть достаточно.
Урок №41. Условный тернарный оператор, оператор sizeof и Запятая
Комментариев: 23
Не понимаю эту функцию
Я написал её в майн чтоб чекнуть её работоспособность и вот что получилось. Получилось что она полностью не рабочая при задаче показателей 0.001 и 0.0012 при епсилоне 0.01 в переводе 0.0001 получалось всегда след результат. При 0.001 выдавало тру при любом изменении нолей спереди или сзади любого числа на 1 оно выдавало ошибку. Как понять где эта черта лимита? Насколько я думал что епсилоном мы задаем черту за которой отличие одного числа от другого не важно на это не так! Ибо числа должны быть 1 к 1 что высветилось тру.
поясните плиз на примерах как оно работает я не втыкаю сижу уже 2 часа тыцяю пробую и так и сяк оно просто рандомно выдает 1 или 0
Проблема из ничего.
Приводим float/double к __int32/__int64 и совершаем над ним операцию AND посредством которой отбрасываем определенное количество младших битов, на ваше усмотрение.
Сравниваем полученные числа.
Если в С++ такая проблема со сравнением дробных чисел, не будет ли логичнее создать отдельный класс? Чтобы каждый объект его состоял из трёх целых чисел (целая часть, дробная часть и количество цифр справа от запятой), а значит не возникало необходимости придумывать функции типа "приблизительно равно" и т.п.
Здравствуйте!
Как правильно сравнивать высоту ( в дес. дробях 0,00 м) саму с собой через одну секунду?
Задача поймать точку прохождения апогея (максимальной высоты).
Написали такое, можете что получше подсказать?
А почему нельзя взять взять за вычисляемый эпсилон среднее арифметическое абсолютных значений сравниваемых величин умноженное на эпсилон? Код вроде попроще будет.
Можно и так наверно, но мне кажется тут берется большее число, потому что всегда надо рассматривать худший случай
Если при сравнении чисел указать тип float вместо double, то результатом будет true, даже при обычном сравнении. Это специфика компилятора или есть еще что-то?
Я тоже заметил что float точный, думаю нужно просто запомнить что double и long double имеют такие костыли.
Почему так уверены? У float будет всё то же самое. Принцип хранения таких чисел ведь одинаковый, что флоат что дабл. А в данном случае у вас просто удачное совпадение. Попробуйте с другими числами и найдёте "неудачные".
Возможно, вы удивитесь, но результат:
Второй вызов не сработал так, как ожидалось. Математика просто ломается, когда дело доходит до нулей.
Почему?
Потому что почти 1(допустим 0.9) — 1 = -0.1. Да это действительно меньше нуля и функция по логике должна возвращать true, но если посмотреть внимательнее можно заметить. что там берется модуль. То есть: fabs(-0.1) = 0.1, а это уже больше нуля
Тяжеловата тема, но интересно.
Наибольшая сложность — не знаешь сразу куда применять.
Тема интересная, но не сразу дается. Код понял "примерно" т.е. поверхностно, чует сердце, буду к нему еще возвращаться. Принцип понятен сразу: как в тестере крутилка: 2 вольта, 20 вольт, 200 вольт и т.д. Воспоминание о аналоговых входах МК меня немного огорчило: там как раз и надо сравнивать небольшие напряжения. Например АКБ -зарядился или нет, сел или еще пойдет… теперь понимаю, почему так часто врут индикаторы заряда батарей. Спасибо, очередной интересный урок!
alt=»Avatar photo» width=»50″ height=»50″ />Юрий :
Пожалуйста Главное — не зацикливайтесь, если что — вернётесь позже к этому уроку.
интересно для написания торгового робота на криптобирже нужно применять функцию approximatelyEqualAbsRel() или нет?
alt=»Avatar photo» width=»50″ height=»50″ />Юрий :
Вы пишете ботов на С++ для криптобирж?
Первый урок, который я вообще не понял :). Видимо, из-за того, что не выспался. Код вообще не понятен. Пытаюсь — не выходит(
Алло, Дед Максим! Ты когда пишешь рукой на листочек строку текста и приближаешься к правому краю и видишь, что последнее слово (если будешь продолжать таким же почерком) не помещается в строку, что делаешь? Правильно. Прижимистей буквы друг к другу тулишь. Это аналоговое представление значений. Цифровое же (то, которое в ЭВМ) — это когда все знаки и расстояния между ними строго одинаковы. И теперь представь себе, что точность — это ширина листа (если листок в клеточку, вообще, идеальная аналогия цифрового представления значений!) И вот тебе надо сравнить заряд электрона и заряд бозона. Что надо сделать? Правильно! Взять листочки по-ширше, т е. установить по-больше точность, иначе не влезающие цифры пропадут и вместо сравниваемых значений вообще какая-то дурь осядет. Но это ещё пол-беды! Подоплёка машинных "мансов" в том, что ЭВМ втихаря дописывает в клеточки левые цифры для заполнения пустующих после значащих цифр клеточек. Ну естественно результаты сравнения 100 — 99.99 и 10 — 9.99 с такими мансами будут не корректными! Да, дык о чём это я? А, вот пример: Требуется сравнить две трёхлитровых банки с жидкостью (молоко, самогон — по вкусу:-). Задаёмся граничным условием — если разница залитых объёмов не превышает одну пипетку (эпсилон) принимаем объёмы как равные. Пипетка — это абсолютный эпсилон, а объём пипетки/объём банки — это относительный эпсилон. А если объёмы сопоставимы с пипеткой (близки нулю)? Тогда Гулливер ловит лилипута, аннексирует у него пипетку (absEpsilon) и если разница меньше этого absEpsilon, то значения объёмов за "ноль" сойдут — не похмелишься (не наешься)!
Радует то, что в реальной жизни чаще требуется сравнивать целые числа. А когда доходит до чисел с плавающей точкой, то там почти всегда не важно ">" или ">=".
alt=»Avatar photo» width=»50″ height=»50″ />Юрий :
Ну это в реальной жизни Та и в реальной жизни бывают исключения.
Кажется у меня отключился мозг после строчки: "Очень часто начинающие разработчики пытаются писать свои собственные функции определения равенства чисел:"
Что обозначает сравнение в с
Условные операции позволяют проверить истинность некоторого условия и оперируют понятиями «истина»/»верно» и «ложь»/»неверно». Причем «истина» (условие верно) соответствует значению 1, а «ложь» (условие не верно) — значению 0. К условным операциям относят операции сравнения и логические операции. Оба этих вида операций применяются к арифметическим операндам, то есть числам. .
Операции сравнения
Операции сравнения (еще называют операциями отношения) позволяют сравнить два значения и возвращают 1 , если выражение истинно, и 0 , если выражение ложно. В языке Си есть следующие операции сравнения:
Операция «равно». Возвращает 1, если оба операнда равны, и 0, если они не равны:
В первом случае переменная c хранит результат выражения a == b . Так как a и b не равны, то это выражение возвратит 0. Поэтому переменная c будет равна 0.
Во втором случае переменная result хранит результат выражения num == 2 + 3; , где в качестве второго операнда выступает результат операции 2 + 3 . Так как переменная num равна 5, то это выражение возвратит 1. Поэтому переменная result будет равна 1.
Операция «не равно». Возвращает 1, если первый операнд не равен второму, и 0, если оба операнда равны:
Операция «больше чем». Возвращает 1, если первый операнд больше второго, и 0, если первый операнд меньше или равен второму:
Операция «меньше чем». Возвращает 1, если первый операнд меньше второго, и 0, если первый операнд больше или равен второму:
Операция «меньше или равно». Возвращает 1, если первый операнд меньше или равен второму, и 0, если первый операнд больше второго:
Операция «больше или равно». Возвращает 1, если первый операнд больше или равен второму, и 0, если первый операнд меньше второго:
Как правило, операции отношения применяются в условных конструкциях типа if. else, которые мы далее рассмотрим.
Логические операции
Логические операции, как правило, применяются к отношениям и объединяют несколько операций отношения. К логическим операциям относят следующие:
Если операнд равен 0, то возвращает 1, иначе возвращает 0.
&& (конъюнкция, логическое умножение)
Возвращает 1, если оба операнда не равны 0. Возвращает 0, если хотя бы один операнд равен 0.
|| (дизъюнкция, логическое сложение)
Возвращает 1, если хотя бы один операнд не равен 0. Возвращает 0, если оба операнда равны 0.
Используем одновременно несколько логических операций и операций сравнения:
Операции сравнения имеют больший приоритет, чем логические операции, поэтому мы могли бы напрямую подставить операции сравнения:
Операции сравнения в C++20
Встреча в Кёльне прошла, стандарт C++20 приведён к более или менее законченному виду (по крайней мере до появления особых примечаний), и я хотел бы рассказать об одном из грядущих нововведений. Речь пойдёт о механизме, который обычно называют operator<=> (стандарт определяет его как «оператор трёхстороннего сравнения», но у него есть неформальное прозвище «космический корабль»), однако я считаю, что область его применения гораздо шире.
У нас не просто будет новый оператор — семантика сравнений претерпит существенные изменения на уровне самого языка.
Даже если ничего больше вы из этой статьи не вынесете, запомните эту таблицу:
Равенство | Упорядочение | |
Базовые | == | <=> |
Производные | != | <, >, <=, >= |
Теперь у нас будет новый оператор, <=>, но, что ещё важнее, операторы теперь систематизированы. Есть базовые операторы и есть производные операторы — каждая группа обладает своими возможностями.
Об этих возможностях мы поговорим коротко во вступлении и рассмотрим подробнее в следующих разделах.
Базовые операторы могут быть обращены (т.е. переписаны с обратным порядком параметров). Производные операторы могут быть переписаны через соответствующий базовый оператор. Ни обращённые, ни переписанные кандидаты не порождают новых функций, они просто являются заменами на уровне исходного кода и отбираются из расширенного набора кандидатов. Например, выражение a < 9 теперь может вычисляться как a.operator<=>(9) < 0, а выражение 10 != b — как !operator==(b, 10). Это значит, что можно будет обойтись одним или двумя операторами там, где для достижения того же поведения сейчас требуется вручную написать 2, 4, 6 или даже 12 операторов. Краткий обзор правил будет представлен ниже вместе с таблицей всех возможных преобразований.
И базовые, и производные операторы можно определять в качестве используемых по умолчанию. В случае базовых операторов это означает, что оператор будет применяться к каждому члену в порядке объявления; в случае производных операторов — что будут использоваться переписанные кандидаты.
Следует отметить, что не существует такого преобразования, при котором оператор одного вида (т.е. равенства или упорядочения) мог бы выражаться через оператор другого вида. Иными словами, столбцы в нашей таблице никак не зависят друг от друга. Выражение a == b никогда не будет вычисляться как operator<=>(a, b) == 0 неявно (но, разумеется, ничто не мешает вам определить свой operator== через operator<=>, если захочется).
Рассмотрим небольшой пример, в котором покажем, как выглядит код до и после применения нового функционала. Мы напишем тип строки, не учитывающий регистр, CIString, объекты которого могут сравниваться как друг с другом, так и с char const*.
В C++17 для нашей задачи потребуется написать 18 функций сравнения:
В C++20 можно обойтись всего лишь 4 функциями:
Я расскажу, что всё это значит, подробнее, но сначала давайте немного вернёмся в прошлое и вспомним, как работали сравнения до стандарта C++20.
Сравнения в стандартах с C++98 по C++17
Операции сравнения почти не менялись с момента создания языка. У нас было шесть операторов: ==, !=, <, >, <= и >=. Стандарт определяет каждый из них для встроенных типов, но в целом они подчиняются одним и тем же правилам. При вычислении любого выражения a @ b (где @ — один из шести операторов сравнения) компилятор ищет функции-члены, свободные функции и встроенные кандидаты с именем operator@, которые могут быть вызваны с типом A или B в указанном порядке. Из них выбирается самый подходящий кандидат. Вот и всё. По сути, все операторы работали одинаково: операция < не отличалась от <<.
Такой простой набор правил легко усвоить. Все операторы абсолютно независимы и эквивалентны. Неважно, что мы, люди, знаем о фундаментальном отношении между операциями == и !=. С точки зрения языка, это одно и то же. Мы же используем идиомы. Например, мы определяем оператор != через ==:
Аналогично, через оператор < мы определяем все остальные операторы отношения. Мы пользуемся этими идиомами, потому что, несмотря на правила языка, мы на самом деле не считаем все шесть операторов эквивалентными. Мы принимаем, что два из них являются базовыми (== и <), а через них уже выражаются все остальные.
В самом деле, стандартная библиотека шаблонов (Standard Template Library) целиком построена на этих двух операторах, и огромное количество типов в эксплуатируемом коде содержит определения только одного из них или их обоих.
Однако оператор < не очень-то подходит на роль базового по двум причинам.
Во-первых, через него нельзя гарантированно выразить другие операторы отношения. Да, a > b означает ровно то же, что b < a, но неверно, что a <= b значит ровно то же, что !(b < a). Последние два выражения будут эквивалентны, если имеется свойство трихотомии, при котором для любых двух значений верно только одно из трёх утверждений: a < b, a == b или a > b. При наличии трихотомии выражение a <=b означает, что мы имеем дело либо с первым, либо со вторым случаем… а это эквивалентно утверждению, что мы не имеем дела с третьим случаем. Поэтому (a <= b) == !(a > b) == !(b < a).
Но что если отношение не обладает свойством трихотомии? Это характерно для отношений частичного порядка. Классический пример — числа с плавающей запятой, для которых любая из операций 1.f < NaN, 1.f == NaN и 1.f > NaN даёт ложь. Поэтому 1.f <= NaN также даёт ложь, но при этом !(NaN < 1.f) — правда.
Единственный способ реализовать оператор <= в общем виде через базовые операторы — это расписать обе операции как (a == b) || (a <b), что является большим шагом назад в том случае, если нам всё же придётся иметь дело с линейным порядком, поскольку тогда будет вызываться не одна функция, а две (например, выражение «abc..xyz9» <= «abc..xyz1» придётся переписать как («abc..xyz9»== «abc..xyz1») || («abc..xyz9» < «abc..xyz1») и дважды сравнивать всю строку целиком).
Во-вторых, оператор < не очень подходит на роль базового из-за особенностей его использования в лексикографических сравнениях. Программисты часто допускают такую ошибку:
Чтобы определить оператор == для коллекции элементов, достаточно один раз применить == к каждому члену, но с оператором < так не получится. С точки зрения этой реализации, множества A и A будут считаться эквивалентными (так как ни одно из них не меньше другого). Чтобы исправить это, следует применить оператор < дважды к каждому члену, кроме последнего:
Наконец, чтобы гарантировать правильную работу сравнений разнотипных объектов — т.е. гарантировать, что выражения a == 10 и 10 == a означают одно и то же, — обычно рекомендуют писать сравнения как свободные функции. На самом деле это вообще единственный способ реализовать такие сравнения. Это неудобно, потому что, во-первых, придётся следить за соблюдением этой рекомендации, а во-вторых, обычно такие функции приходится объявлять скрытыми друзьями для более удобной реализации (т.е. внутри тела класса).
Заметим, что не всегда при сравнениях разнотипных объектов требуется писать именно operator==(X, int); они могут также подразумевать случаи, когда int может неявно приводиться к X.
Подведём итоги по правилам до стандарта C++20:
- Все операторы обрабатываются одинаково.
- Мы используем идиомы для облегчения реализации. Операторы == и < мы принимаем за базовые идиомы и выражаем остальные операторы отношения через них.
- Вот только оператор < не очень подходит на роль базового.
- Важно (и рекомендовано) писать сравнения разнотипных объектов как свободные функции.
Новый базовый оператор упорядочения: <=>
Самое значительное и заметное изменение в работе сравнений в C++20 — это добавление нового оператора — operator<=>, оператора трёхстороннего сравнения.
С трёхсторонними сравнениями мы уже знакомы по функциям memcmp/strcmp в C и basic_string::compare() в C++. Все они возвращают значение типа int, которое представлено произвольным положительным числом, если первый аргумент больше второго, 0 — если они равны, и произвольным отрицательным числом в противном случае.
Оператор «космический корабль» возвращает не значение типа int, а объект, принадлежащий к одной из категорий сравнения, чьё значение отражает вид отношения между сравниваемыми объектами. Существует три основных категории:
- strong_ordering: отношение линейного порядка, при котором равенство подразумевает взаимозаменяемость элементов (т.е. (a <=> b) == strong_ordering::equal подразумевает, что для всех подходящих функций f имеет место f(a) == f(b). Термину «подходящая функция» намеренно не даётся чёткого определения, но к таковым не относятся функции, которые возвращают адреса своих аргументов или capacity() вектора и т.п. Нас интересуют только «существенные» свойства, что тоже очень расплывчато, но можно условно считать, что речь идёт о значении типа. Значение вектора — это содержащиеся в нём элементы, но не его адрес и т.п.). Эта категория включает в себя следующие значения: strong_ordering::greater, strong_ordering::equal и strong_ordering::less.
- weak_ordering: отношение линейного порядка, при котором равенство определяет лишь некоторый класс эквивалентности. Классический пример — нечувствительное к регистру сравнение строк, когда два объекта могут быть weak_ordering::equivalent, но не равны в строгом смысле (этим объясняется замена слова equal на equivalent в имени значения).
- partial_ordering: отношение частичного порядка. В этой категории к значениям greater, equivalent и less (как в weak_ordering) добавляется ещё одно — unordered («неупорядоченно»). С его помощью можно выражать отношения частичного порядка в системе типов: 1.f <=> NaN даёт значение partial_ordering::unordered.
Категории более сильного порядка могут неявно приводиться к категориям более слабого порядка (т.е. strong_ordering приводимо к weak_ordering). При этом текущий вид отношения сохраняется (т.е. strong_ordering::equal превращается в weak_ordering::equivalent).
Значения категорий сравнения можно сравнивать с литералом 0 (не с любым int и не с int, равным 0, а просто с литералом 0) с помощью одного из шести операторов сравнения:
Именно благодаря сравнению с литералом 0 мы можем реализовывать операторы отношения: a @ b эквивалентно (a <=> b) @ 0 для каждого из таких операторов.
Например, 2 < 4 можно вычислить как (2 <=> 4) < 0, что превращается в strong_ordering::less < 0 и даёт значение true.
На роль базового элемента оператор <=> подходит намного лучше, чем оператор <, поскольку он избавлен от обеих проблем последнего.
Во-первых, выражение a <= b гарантированно эквивалентно (a <=> b) <= 0 даже при частичном порядке. Для двух неупорядоченных значений a <=> b даст значение partial_ordered::unordered, а partial_ordered::unordered <= 0 даст false, что нам и требуется. Это возможно потому, что <=> может вернуть больше разновидностей значений: так, категория partial_ordering содержит четыре возможных значения. Значение типа bool может быть только true или false, поэтому раньше мы не могли различать сравнения упорядоченных и неупорядоченных значений.
Для большей ясности рассмотрим пример отношения частичного порядка, не связанный с числами с плавающей запятой. Допустим, мы хотим добавить типу int состояние NaN, где NaN — это просто значение, которое не образует упорядоченной пары ни с одним задействованным значением. Сделать это можно, используя для его хранения std::optional:
Оператор <= возвращает правильное значение потому, что теперь мы можем выразить больше информации на уровне самого языка.
Во-вторых, чтобы получить всю необходимую информацию, достаточно один раз применить <=>, что облегчает реализацию лексикографического сравнения:
Более подробный разбор см. в P0515 — исходном предложении по добавлению operator<=>.
Новые возможности операторов
Мы не просто получаем в своё распоряжение новый оператор. В конце концов, если бы показанный выше пример с объявлением структуры A говорил лишь о том, что вместо x < y теперь придётся всякий раз писать (x <=> y) < 0, это никому бы не понравилось.
Механизм разрешения сравнений в C++20 заметно отличается от старого подхода, но это изменение напрямую связано с новой концепцией двух базовых операторов сравнения: == и <=>. Если раньше это была идиома (запись через == и <), которой пользовались мы, но о которой не знал компилятор, то теперь и он будет понимать это различие.
Ещё раз приведу таблицу, которую вы уже видели в начале статьи:
Равенство | Упорядочение | |
Базовые | == | <=> |
Производные | != | <, >, <=, >= |
Каждый из базовых и производных операторов получил новую способность, о чём я скажу пару слов далее.
Обращение базовых операторов
В качестве примера возьмём тип, который может сравниваться только с int:
С точки зрения старых правил, нет ничего удивительного в том, что выражение a == 10 работает и вычисляется как a.operator==(10).
Но как насчёт 10 == a? В C++17 это выражение считалось бы явной синтаксической ошибкой. Не существует такого оператора. Чтобы такой код заработал, пришлось бы писать симметричный operator==, который бы сначала брал значение int, а затем A… а реализовывать это пришлось бы в виде свободной функции.
В C++20 базовые операторы могут быть обращены. Для 10 == a компилятор найдёт кандидат operator==(A, int) (на самом деле это функция-член, но для наглядности я пишу её здесь как свободную функцию), а затем дополнительно — вариант с обратным порядком параметров, т.е. operator==(int, A). Этот второй кандидат совпадает с нашим выражением (причём идеально), так что его мы и выберем. Выражение 10 == a в C++20 вычисляется как a.operator==(10). Компилятор понимает, что равенство симметрично.
Теперь расширим наш тип так, чтобы его можно было сравнивать с int не только через оператор равенства, но и через оператор упорядочения:
Опять же, выражение a <=> 42 работает прекрасно и вычисляется по старым правилам как a.operator<=>(42), но вот 42<=> a было бы неправильно с точки зрения C++17, даже если бы оператор <=> уже существовал в языке. Но в C++20 operator<=>, как и operator==, симметричен: он распознаёт обращённые кандидаты. Для 42 <=> a будет найдена функция-член operator<=>(A, int) (опять же, я пишу её здесь как свободную функцию просто для большей наглядности), а также синтетический кандидат operator<=>(int, A). Этот обращённый вариант точно соответствует нашему выражению — его и выбираем.
Однако 42 <=> a вычисляется НЕ как a.operator<=>(42). Так было бы неправильно. Это выражение вычисляется как 0 <=> a.operator<=>(42). Попробуйте сами догадаться, почему эта запись — правильная.
Важно отметить, что никаких новых функций компилятор не создаёт. При вычислении 10 == a не появился новый оператор operator==(int, A), а при вычислении 42 <=> a не появился operator<=>(int, A). Просто два выражения переписаны через обращённые кандидаты. Повторю: никаких новых функций не создаётся.
Также обратите внимание, что запись с обратным порядком параметров доступна только для базовых операторов, а для производных — нет. То есть:
Переписывание производных операторов
Вернёмся к нашему примеру со структурой A:
Возьмём выражение a != 17. В C++17 это синтаксическая ошибка, потому что не существует оператора operator!=. Однако в C++20 для выражений, содержащих производные операторы сравнения, компилятор будет также искать соответствующие им базовые операторы и выражать через них производные сравнения.
Мы знаем, что в математике операция != по сути означает НЕ ==. Теперь это известно и компилятору. Для выражения a!= 17 он будет искать не только операторы operator!=, но и operator== (а также, как в предыдущих примерах, обращённые operator==). Для данного примера мы нашли оператор равенства, который нам почти подходит, — нужно только переписать его в соответствии с желаемой семантикой: a != 17 будет вычисляться как !(a == 17).
Аналогично, 17 != a вычисляется как !a.operator==(17), что является одновременно и переписанным, и обращённым вариантом.
Похожие преобразования проводятся и для операторов упорядочения. Если бы мы написали a < 9, то попытались бы (безуспешно) найти operator<, а также рассмотрели бы базовые кандидаты: operator<=>. Соответствующая замена для операторов отношения выглядит так: a @ b (где @ — один из операторов отношения) вычисляется как (a <=> b) @ 0. В нашем случае — a.operator<=>(9) < 0. Аналогично, 9 <= a вычисляется как 0 <= a.operator<=>(9).
Заметим, что, как и в случае с обращением, компилятор не создаёт никаких новых функций для переписанных кандидатов. Они просто по-другому вычисляются, а все трансформации проводятся только на уровне исходного кода.
Вышесказанное приводит меня к следующему совету:
ТОЛЬКО БАЗОВЫЕ ОПЕРАТОРЫ: В своём типе определяйте только базовые операторы (== и <=>).
Поскольку базовые операторы дают весь набор сравнений, то и определять достаточно только их. Это значит, что вам понадобится только 2 оператора для сравнения однотипных объектов (вместо 6, как сейчас) и только 2 оператора для сравнения разнотипных объектов (вместо 12). Если вам нужна только операция равенства, то достаточно написать 1 функцию для сравнения однотипных объектов (вместо 2) и 1 функцию для сравнения разнотипных объектов (вместо 4). Класс std::sub_match представляет собой крайний случай: в C++17 в нём используется 42 оператора сравнения, а в C++20 — только 8, при этом функциональность никак не страдает.
Так как компилятор рассматривает также обращённые кандидаты, все эти операторы можно будет реализовывать как функции-члены. Больше не придётся писать свободные функции только ради сравнения разнотипных объектов.
Особые правила поиска кандидатов
Как я уже упоминал, поиск кандидатов для a @ b в C++17 происходил по следующему принципу: находим все операторы operator@ и выбираем из них наиболее подходящий.
В C++20 используется расширенный набор кандидатов. Теперь мы будем искать все operator@. Пусть @@ — это базовый оператор для @ (это может быть один и тот же оператор). Мы также находим все operator@@ и для каждого из них добавляем его обращённую версию. Из всех этих найденных кандидатов выбираем наиболее подходящий.
Заметьте, что перегрузка оператора разрешается за один-единственный проход. Мы не пытаемся подставлять разные кандидаты. Сначала мы собираем их все, а затем выбираем из них наилучший. Если такого не существует, поиск, как и раньше, заканчивается неудачей.
Теперь у нас гораздо больше потенциальных кандидатов, а значит и больше неопределённости. Рассмотрим следующий пример:
В C++17 у нас был только один кандидат для x != y, а теперь их три: x.operator!=(y), !x.operator==(y) и !y.operator==(x). Что же выбрать? Они все равнозначны! (Примечание: кандидата y.operator!=(x) не существует, так как обращать можно только базовые операторы.)
Для снятия этой неопределённости введены два дополнительных правила. Необращённые кандидаты предпочтительнее обращённых; непереписанные кандидаты предпочтительнее переписанных. Тогда получается, что x.operator!=(y) «главнее» !x.operator==(y), а тот «главнее» !y.operator==(x). Этот принцип согласуется со стандартными правилами, по которым «побеждает» наиболее точный вариант.
Ещё одно замечание: на этапе поиска нас не интересует тип возвращаемого значения кандидатов operator@@. Мы просто находим их. Нас интересует только, являются ли они наилучшим выбором или нет.
Неудачный исход при поиске теперь тоже выглядит по-другому. Если наилучший кандидат — переписанный или обращённый (например, мы написали x < y, а наилучший кандидат — это (x <=> y) < 0), но корректно переписать или обратить сравнение невозможно (например, x <=> y возвращает void или какой-то иной тип, потому что мы вообще пишем на DSL), то программа считается некорректной. Возвращаться и искать другой подходящий вариант мы уже не будем. В случае с операцией равенства мы принимаем, что никакой тип возвращаемого значения кроме bool не совместим с переписанными кандидатами (логика здесь такая: если operator== не возвращает bool, можем ли мы считать такую операцию операцией равенства?)
Для выражения d1 < d2 будут найдены два кандидата: #1 и #2. Наилучший вариант — #2, так как он является точным совпадением, значит, его и выбираем. Поскольку это переписанный кандидат, то d1 < d2 вычисляется как (d1 <=> d2) < 0. Но это некорректное выражение, ведь нельзя сравнивать void с 0 — значит, и всё сравнение некорректно. Заметьте, что после этой неудачи мы уже не будем совершать какие-либо действия, чтобы выбрать кандидат #1.
Краткий обзор правил
Очевидно, что эти правила сложнее тех, что были в C++17, но я привожу их полностью в этом небольшом разделе. Здесь не будет сносок, посвящённых каким-то особым случаям или исключениям. Просто запомните самые главные принципы:
- Обращение доступно только для базовых операторов
- Переписываться могут только производные операторы (через соответствующие базовые)
- При поиске кандидатов за один проход ищутся все операторы с данным именем, а также все их обращённые и переписанные версии
- Если наилучший кандидат является переписанной или обращённой версией и при этом такая замена является недопустимой, программа считается некорректной.
Для ясности я привожу таблицу со всеми возможными преобразованиями на уровне исходного кода. В каждом случае выражение в первом столбце имеет больший приоритет, чем выражение во втором, а то, в свою очередь, имеет больший приоритет, чем выражение в третьем столбце (при прочих равных условиях). Обратите внимание, что второй и третий столбцы содержат только базовые операторы:
Исходная операция | Вариант 1 | Вариант 2 |
a == b | b == a | |
a != b | !(a == b) | !(b == a) |
a <=> b | 0 <=> (b <=> a) | |
a < b | (a <=> b) < 0 | (b <=> a) > 0 |
a <= b | (a <=> b) <= 0 | (b <=> a) >= 0 |
a > b | (a <=> b) > 0 | (b <=> a) < 0 |
a >= b | (a <=> b) >= 0 | (b <=> a) <= 0 |
Варианты с «космическим кораблём» в правом столбце обычно пишутся с тем же оператором, что и в исходной версии, т.е. a < b пишется как 0 < (b <=> a), но я написал их с противоположными знаками, чтобы нагляднее показать, как меняется знак в переписанной версии.
Определение сравнений для использования по умолчанию
Среди прочего в C++17 раздражает необходимость подробно расписывать поэлементные лексикографические сравнения. Это занятие утомительно и чревато ошибками. Напишем полный набор операторов для линейно упорядоченного типа с тремя членами:
Ещё лучше было бы использовать какой-нибудь std::tie(), но это всё равно утомительно.
Теперь давайте попробуем написать ту же структуру, следуя моему совету: определять только базовые операторы:
Тут не просто меньше кода. Сама реализация <=> гораздо проще для понимания по сравнению с реализацией <. Она очевидней, поскольку полное сравнение можно выполнить за один проход. Проверки c != 0 не дадут нам продолжить, если мы обнаружим пару неравных значений, и каким бы отношением ни было выражено это неравенство (меньше или больше), это будет окончательный результат сравнения.
В итоге получается обычное поэлементное лексикографическое сравнение по умолчанию. А в C++20 достаточно просто сказать компилятору, что мы хотим:
Нужно явно указать, какие операторы сравнения должен сгенерировать компилятор по умолчанию. Наш код можно ещё упростить, если категорию сравнения определять автоматически:
Можно пойти ещё дальше. В типичном сценарии, когда требуется обычное поэлементное сравнение на равенство и отношение, достаточно определить только один оператор:
Это единственный случай, когда компилятор сгенерирует оператор сравнения, который вы сами не писали. Последние два варианта абсолютно идентичны: у нас есть и заданный по умолчанию operator==, и заданный по умолчанию operator<=>.
Темы будущих статей
В этой статье мы рассмотрели основы сравнений в C++20: как работают синтетические кандидаты и как они находятся. Мы также коротко рассмотрели трёхстороннее сравнение и особенности его реализации. У меня в запасе есть ещё несколько интересных тем, которые тоже стоит осветить, но я стараюсь писать не слишком длинные статьи, так что ждите новых постов.
Примечание переводчика
Команда PVS-Studio с интересом познакомилась с этой статьей, так как нам в ближайшее время предстоит реализовать поддержку нового оператора <=> в анализаторе. А поскольку статья очень полезная и хорошо всё объясняет, мы решили сделать её перевод для хабра-сообщества. На наш взгляд, это очень нужное нововведение языка, так как по нашему опыту операторы сравнения очень часто содержат ошибки (см. статью «Зло живёт в функциях сравнения»). Теперь С++ программистам жить станет проще и ошибок данного типа будет меньше.
Заодно возникла идея создать в PVS-Studio новую диагностику для поиска некорректно написанных операторов <, которые были описаны в статье:
Подобный код может присутствовать в старых больших проектах. Возможно, и ещё какие-то диагностики сделаем. Надо подумать.