2

We upgrade our codebase from c++17 to c++20 lately, and some code does not work as before. I take the following simplified sample code as show case here.

None of the overloaded compare operators is called. Instead, it converts to const char* to compare. Why is that?

#include <iostream>
#include <string_view>

class FooString {
 public:
  FooString& operator=(const FooString& src) {
    if (this != &src) {
      m_data = src.m_data;
    }
    return *this;
  }
  FooString& operator=(FooString&& src) {
    m_data = std::move(src.m_data);
    return *this;
  }
  FooString& operator=(const char* value) {
    m_data = value;
    return *this;
  }
  FooString& operator=(std::string value) {
    m_data = std::move(value);
    return *this;
  }

  bool operator==(const FooString& value) const {
    return (0 == compare(m_data, value.m_data));
  }

  bool operator!=(const FooString& value) const{
    printf("__%d__\n", __LINE__);
    return (compare(m_data, value.m_data));
  }

  bool operator<=(const FooString& value) const {
    printf("__%d__\n", __LINE__);
    return (0 >= compare(m_data, value.m_data));
  }

  bool operator>=(const FooString& value) const{
    printf("__%d__\n", __LINE__);
    return (0 <= compare(m_data, value.m_data));
  }

  bool operator<(const FooString& value) const {
    printf("__%d__\n", __LINE__); // this was called before c++20
    return (0 > compare(m_data, value.m_data));
  }

  bool operator>(const FooString& value) const {
    printf("__%d__\n", __LINE__);
    return (0 < compare(m_data, value.m_data));
  }

  operator const char*() const { 
    printf("__%d__\n", __LINE__); // this function gets called with c++20
    return m_data.c_str();
  }
  const std::string& str() const {
    return m_data;
  }
  const char* c_str() const {
    return m_data.c_str();
  }

 protected:
  virtual int compare(std::string_view str1, std::string_view str2) const {
      return str1.compare(str2);
  }

 private:
  std::string m_data;
};

int main()
{
  FooString str1;
  str1 = "test1";
  FooString str2;
  str2 = "test1";
  std::tuple<const FooString&> k1(std::tie(str1));
  std::tuple<const FooString&> k2(std::tie(str2));
  if (k1 < k2) {
    printf("k1 < k2\n"); 
  } else {
    printf("k1 >= k2\n"); 
  }
  return 0;
}

Live Demo Switch between -std=c++17 and -std=c++20 to see the difference.

I search a bit around and learnt this is due to c++20 impose totally ordering for tuple reference. I thought we have provided the totally ordering operators !=, ==, etc. no? or why the overloaded operators are shadowed by the conversion operator const char*? Now the comparsion with pointer address with c++20 depends on the machine ordering...

any insights are welcome. Also this is a breaking feature from c++20, no?

5
  • A side note: there's no need to use printf in c++. std::cout was available more or less since the beginning, and recently (c++23) we also have the more modern std::print. See: 'printf' vs. 'cout' in C++. Commented Aug 15 at 10:48
  • thanks! I take the example from the old code base and did not revise too much Commented Aug 15 at 10:54
  • @463035818_is_not_an_ai, I updated the live demo to save time for readers. now it shows more clearly where is called between c++17 and c++20. Commented Aug 15 at 11:10
  • 2
    fwiw, it is a breaking change. And its nasty because it breaks silently. Unfortunately breaking changes cannot be avoided when there should be forward progression. Funnily if you google for "c++ breaking change" you find reddit.com/r/cpp/comments/ryg2hc/… which is just about the change you hit. Commented Aug 15 at 13:47
  • so this is not just applicable to std::tuple, but all std containers!!!!!! Commented Aug 18 at 10:07

1 Answer 1

7

Since C++20, std::tuple's relational comparison is defined in terms of synth-three-way, which uses the three-way comparison operator <=> if available, and falls back to <.

It is valid to apply <=> to const FooString&, because const FooString& is convertible to const char*, and const char* is three-way comparable:

auto f(const FooString& a, const FooString& b) {
    return a <=> b; // OK, `a` and `b` are converted to `const char*` and then compared
}

So std::tuple uses that operator in its comparison.

You need to either provide operator<=> for const FooString&, or else define operator<=> as deleted so that std::tuple can use the fallback. (Or better yet, remove the implicit conversion if possible.)

Sign up to request clarification or add additional context in comments.

6 Comments

i was searching for <=> for const char* on cppreference but I couldnt find it. Do you know where it is defined?
so when fallback, the compiler prefers the "const char*" that has spaceship operator to the customized operator <?
"Use <=>" is the default, not a fallback, and tuple doesn't know where the operator <=> comes from.
aha, @cpplearner, do you mean: default <=>, even by implicit conversion, if it is not available then fallback: "<"?
To suppress the synth-three-way, #if __cplusplus >= 202002 void operator<=>(FooString const&, FooString const&) noexcept = delete; #endif

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.