title: More silent behaviour changes with C++20 three-way comparison
body: <p>To my surprise, I ran into another snag like <em><a href="https://stackoverflow.com/questions/65648897/c20-behaviour-breaking-existing-code-with-equality-operator">C++20 behaviour breaking existing code with equality operator?</a></em>.</p>
  <p>Consider a simple case-insensitive key type, to be used with, e.g., <code>std::set</code> or <code>std::map</code>:</p>
  <pre><code>// Represents case insensitive keys
  struct CiKey : std::string {
      using std::string::string;
      using std::string::operator=;
  
      bool operator&lt;(CiKey const&amp; other) const {
          return boost::ilexicographical_compare(*this, other);
      }
  };
  </code></pre>
  <p>Simple tests:</p>
  <pre><code>using KeySet   = std::set&lt;CiKey&gt;;
  using Mapping  = std::pair&lt;CiKey, int&gt;; // Same with std::tuple
  using Mappings = std::set&lt;Mapping&gt;;
  
  int main()
  {
      KeySet keys { &quot;one&quot;, &quot;two&quot;, &quot;ONE&quot;, &quot;three&quot; };
      Mappings mappings {
          { &quot;one&quot;, 1 }, { &quot;two&quot;, 2 }, { &quot;ONE&quot;, 1 }, { &quot;three&quot;, 3 }
      };
  
      assert(keys.size() == 3);
      assert(mappings.size() == 3);
  }
  </code></pre>
  <ul>
  <li><p>Using C++17, both asserts pass (<a href="https://godbolt.org/z/Ps5qo7" rel="noreferrer">Compiler Explorer</a>).</p>
  </li>
  <li><p>Switching to C++20, the second assert fails (<a href="https://godbolt.org/z/3asYvW" rel="noreferrer">Compiler Explorer</a>)</p>
  <blockquote>
  <p>output.s: ./example.cpp:28: int main(): Assertion `mappings.size() ==
  3' failed.</p>
  </blockquote>
  </li>
  </ul>
  <hr />
  <h2>Obvious Workaround</h2>
  <p>An obvious work-around is to conditionally supply <code>operator&lt;=&gt;</code> in C++20 mode: <a href="https://godbolt.org/z/eK9oP5" rel="noreferrer">Compile Explorer</a></p>
  <pre><code>#if defined(__cpp_lib_three_way_comparison)
      std::weak_ordering operator&lt;=&gt;(CiKey const&amp; other) const {
          if (boost::ilexicographical_compare(*this, other)) {
              return std::weak_ordering::less;
          } else if (boost::ilexicographical_compare(other, *this)) {
              return std::weak_ordering::less;
          }
          return std::weak_ordering::equivalent;
      }
  #endif
  </code></pre>
  <h2>Question</h2>
  <p>It surprises me that I ran into another case of breaking changes - where C++20 changes behaviour of code without diagnostic.</p>
  <p>On my <a href="https://en.cppreference.com/w/cpp/utility/tuple/operator_cmp" rel="noreferrer">reading of <code>std::tuple::operator&lt;</code></a> it should have worked:</p>
  <blockquote>
  <p>3-6) Compares <code>lhs</code> and <code>rhs</code> lexicographically by <code>operator&lt;</code>, that is,
  compares the first elements, if they are equivalent, compares the
  second elements, if those are equivalent, compares the third elements,
  and so on. For non-empty tuples, (3) is equivalent to</p>
  <pre><code>if (std::get&lt;0&gt;(lhs) &lt; std::get&lt;0&gt;(rhs)) return true;
  if (std::get&lt;0&gt;(rhs) &lt; std::get&lt;0&gt;(lhs)) return false;
  if (std::get&lt;1&gt;(lhs) &lt; std::get&lt;1&gt;(rhs)) return true;
  if (std::get&lt;1&gt;(rhs) &lt; std::get&lt;1&gt;(lhs)) return false;
  ...
  return std::get&lt;N - 1&gt;(lhs) &lt; std::get&lt;N - 1&gt;(rhs);
  </code></pre>
  </blockquote>
  <p>I understand that technically these don't apply since C++20, and it gets replaced by:</p>
  <blockquote>
  <p>Compares <code>lhs</code> and <code>rhs</code> lexicographically by synthesized three-way
  comparison (see below), that is, compares the first elements, if they
  are equivalent, compares the second elements, if those are equivalent,
  compares the third elements, and so on</p>
  </blockquote>
  <p>Together with</p>
  <blockquote>
  <p>The &lt;, &lt;=, &gt;, &gt;=, and != operators are synthesized from <code>operator&lt;=&gt;</code> and <code>operator==</code> respectively. <em>(since C++20)</em></p>
  </blockquote>
  <p>The thing is,</p>
  <ul>
  <li><p>my type doesn't define <code>operator&lt;=&gt;</code> nor <code>operator==</code>,</p>
  </li>
  <li><p>and as <a href="https://stackoverflow.com/questions/48065905/is-the-three-way-comparison-operator-always-efficient">this answer points out</a> providing <code>operator&lt;</code> in addition would be fine and should be used when evaluating simple expressions like <code>a &lt; b</code>.</p>
  </li>
  </ul>
  <ol>
  <li>Is the behavior change in C++20 correct/on purpose?</li>
  <li>Should there be a diagnostic?</li>
  <li>Can we use other tools to spot silent breakage like this? It feels like scanning entire code-bases for usage of user-defined types in <code>tuple</code>/<code>pair</code> doesn't scale well.</li>
  <li>Are there other types, beside <code>tuple</code>/<code>pair</code> that could manifest similar changes?</li>
  </ol>