понедельник, 11 августа 2025 г.

[prog.c++] Оператор <=> и оператор ==

Надеюсь, многие знают, что в C++20 появилась замечательная штука operator<=>. Стоит его определить, как компилятор на его основе выводит другие операторы сравнения, вроде operator< или operator>=.

Причем, что особенно хорошо, можно попросить компилятор сгенерировать operator<=> для нас автоматически:

struct example {
  ... // Какие-то поля.

  auto operator<=>(const example &) const = default;
};

И все. Остальное за нас сделает компилятор.

Однако, думаю, не все знают, что если нам не подходит штатная реализация и мы пишем operator<=> сами, то компилятор потребует, чтобы мы еще и определили свой собственный operator==.

Например, допустим, что у нас в example три поля, а в сравнении должно участвовать только два из них:

struct example {
  int m_a;
  int m_b;
  int m_c;

  auto operator<=>(const example & o) const noexcept
  {
    return std::tie(m_a, m_c) <=> std::tie(o.m_a, o.m_c);
  }
};

В этом случае мы внезапно© обнаружим, что у нас нет operator==.

Таков путь: если мы определили свой operator<=>, то компилятор не будет автоматически выводить равенство и неравенство, а потребует, чтобы мы предоставили operator==.

Может быть вам будет интересно почему?

Если да, то лучше всего прочитать раздел с мотивацией из предложения P1185 и вот этот ответ на Stackoverflow. В двух же словах: реализация сравнения на строгое равенство на базе operator<=> может быть весьма неэффективна.

Например, представим, что мы сравниваем две строки: s1="AA" и s2="AAA".

При лексикографическом сравнении s1 меньше, чем s2: общая подстрока в s1 и s2 совпадает, но в s1 символов меньше.

Когда мы для наших строк s1 и s2 вызываем operator<=>, то этот оператор пытается выяснить как именно соотносятся s1 и s2: меньше ли s1, чем s2, или же s1 больше, чем s2, или же они равны.

Теперь допустим, что в s1 находится миллион символов "A", а в s2 -- миллион и один символ "A". При вызове operator<=> придется сравнить миллион символов в общей подстроке прежде чем мы сможем понять, что s1 все-таки меньше, т.к. у нее символов меньше.

При этом мы не можем прервать сравнение раньше. Допустим, что в s1 будет девятьсот девяносто девять тысяч и девятьсот девяносто девять символов "A", а за ними единственный символ "B", тогда как в s2 все миллион + один символ -- это "A". В таком случае строка s1 окажется больше, не смотря на то, что символов в ней меньше.

Однако, если нам не нужно выяснять общее соотношение между s1 и s2, а достачно просто понять равны ли они или нет, то мы можем начать со сравения длин и лишь затем переходить к посимвольному сравнению.

Когда компилятор по нашей просьбе сам генерирует operator<=>, то он эти фокусы учитывает (под катом небольшой тестовый бенчмарк, который позволяет убедится, что сравнение двух неравных векторов требует минимального времени). Однако, если operator<=> предоставляет пользователь, то у компилятора нет уверенности в том, что пользователь написал свой operator<=> с учетом возможностей оптимизации операции "строгое равенство". И просит нас взять ответственность за operator== на себя.

Вот несложный код, который позволяет убедиться, что программа не делает сравнения каждого из миллиона элементов вектора внутри структуры owner:

#include <chrono>
#include <iostream>
#include <string>
#include <vector>

struct owner
{
   std::vector< std::string > m_strings;

   auto operator<=>(const owner &) const = default;
};

int main()
{
   owner first;
   for( std::size_t i = 0; i != 1'000'000; ++i )
      first.m_strings.emplace_back( "just a simple string" );

   owner second{ first };
   second.m_strings.pop_back();

   const auto started_at = std::chrono::high_resolution_clock::now();
   int valid_results{};
   forint i = 0; i < 100'000; ++i )
      if( first != second )
         ++valid_results;
   const auto finished_at = std::chrono::high_resolution_clock::now();

   std::cout << "results: " << valid_results
         << ", time: "
         << std::chrono::duration_cast< std::chrono::microseconds >(
               finished_at - started_at ).count()
         << " us" << std::endl;
}

Комментариев нет: