GCC and Clang differences on operator< when implicit conversion is available

Viewed 50

I'm puzzled by the differences arising when compiling the following minimal code on gcc (11.2.0) and clang (12.0.1). I run my tests using -std=c++20, but c++17 seem to make no difference.

struct Type1 {
  explicit Type1(int i) : m_(i) {}
  int m_;
};

struct MyClass {
  template<typename T>
  /*explicit*/ MyClass(T &&t) : m_(t) {}
  std::variant<Type1> m_;
};

bool operator<(const MyClass &lhs, const MyClass &rhs) {
  return lhs.m_ < rhs.m_;
}

bool operator<(const Type1 &lhs, const Type1 &rhs) {
  return lhs.m_ < rhs.m_;
}

int main() {
  MyClass mc1(Type1(42));
  MyClass mc2(Type1(9000));
  std::cout << (mc1 < mc2 ? "yup" : "nope") << std::endl;
}

Both compilers compile the code with no warning. GCC will print yup, as one might expect, but if I run the clang one, I get a segfault. It turns out bool operator<(const MyClass &lhs, const MyClass &rhs) recursively calls itself by implicitly converting Type1 into MyClass.

Question 1: Which of the two behaviors sticks to the standard?

I find it hard to understand how gcc can even generate code for bool operator<(const MyClass &lhs, const MyClass &rhs) before reaching the definition of bool operator<(const Type1 &lhs, const Type1 &rhs). So I get why clang would do what it does.

I also ran a few variations on the theme:

  1. If I let MyClass(T &&t) to be marked explicit, then clang does not compile, since std::variant::operator< doesn't know any comparison to use. To gcc is like nothing has changed.
  2. If I switch the order of declaration of both operator<, then it all works fine in both compilers, as expected.
  3. If I change std::variant by std::tuple, I get the same different behaviors between gcc and clang. So there is probably nothing special in <variant>, but maybe the different STL implementations play a role here?
  4. If instead of std::variant<Type1> I use directly Type1, then both compilers will produce the infinite recursion issue, and if on top of that I add the explicit keyword, then both compilers will fail to compile. In other words, they both agree.

Question 2: Why does this indirection (having std::variant or std::tuple) make a difference in this case?

To my understanding, since these containers will basically call the comparator of the class it holds, they should be known when compiling that piece of code. So it seems to me that gcc marks that it needs to use the comparator from std::variant when compiling bool operator<(const MyClass &lhs, const MyClass &rhs), but only actually generates code after it gets to know bool operator<(const Type1 &lhs, const Type1 &rhs), and I have no idea what is the mechanism used for that.

Finally, I guess my best defense against this is to never allow implicit conversion to begin with. Any other advice?

0 Answers
Related