Which of two conversion operators must be selected by C++ compiler?
Asked Answered
C

1

2

A class can declare several conversion operators. In particular it can be conversion operators to some type and to const-reference of the same type. Which of the two conversion operators must be selected in case of requested conversion to that type?

Consider an example:

#include <iostream>

struct B {};
static B sb;

struct A {
    operator B() { std::cout << "operator B() "; return sb; }
    operator const B &() { std::cout << "operator const B &() "; return sb; }
};

int main() {
    A a;
    [[maybe_unused]] B b(a);
}

Here Clang selects operator B(), MSVC selects operator const B &(), and GCC complains about ambiguity of the selection:

<source>:13:27: error: call of overloaded 'B(A&)' is ambiguous
   13 |     [[maybe_unused]] B b(a);
      |                           ^
<source>:3:8: note: candidate: 'constexpr B::B(const B&)'
    3 | struct B {};
      |        ^
<source>:3:8: note: candidate: 'constexpr B::B(B&&)'

Demo: https://gcc.godbolt.org/z/874h7h3d1

Which of the compilers is right?

Capillaceous answered 12/9, 2021 at 5:46 Comment(4)
At least msvc's choice is so wrong, if they are actually obeying the standard, it has a bug. Which sustains faith.Intratelluric
It should be ambiguous, C++ shouldn't be able to distinguish based on return type only. On another note: typecast operators should be used with care anyway. I only use them when I am sure I am writing a wrapper for something that should behave like the underlying type.Oram
Related recent question (possibly even a dupe target?): The selected overload from the conversion functions set is not consistent between GCC and Clang. The only difference in this example is the const qualifier on the return of B& overload in A.Vieva
@dfrib, thanks for the reference, I missed it by myself. In that question, in class B the move constructor is deleted, so there is never an ambiguity as here. But I would agree that an answer on it will partially answer this question as well.Capillaceous
T
2

The program is ill-formed and rejected by GCC is correct here but the diagnosis can arguably say it is not completely correct. For this declaration B b(a);, it is direct-initialization of an object of class B from the initializer a of type A, according to [over.match.copy] p1

Assuming that “cv1 T” is the type of the object being initialized, with T a class type, the candidate functions are selected as follows:

  • The converting constructors of T are candidate functions.
  • When the type of the initializer expression is a class type “cv S”, conversion functions are considered. The permissible types for non-explicit conversion functions are T and any class derived from T. When initializing a temporary object ([class.mem]) to be bound to the first parameter of a constructor where the parameter is of type “reference to cv2 T” and the constructor is called with a single argument in the context of direct-initialization of an object of type “cv3 T”, the permissible types for explicit conversion functions are the same; otherwise there are none.

For converting constructors, they are copy/move constructors of B, however, [over.best.ics#general-4] prohibits the user-defined conversion sequence to apply to the target to match the parameter of the constructor

However, if the target is

  • the first parameter of a constructor or
  • [...]

and the constructor or user-defined conversion function is a candidate by

  • [...]
  • [over.match.copy], [over.match.conv], or [over.match.ref] (in all cases), or
  • [...]

user-defined conversion sequences are not considered.

Hence, the copy/move constructors of B are not viable functions. The ambiguity arises from the viable functions A::operator B() and A::operator const B &(), since the implicit parameter objects of them both have type A& and the corresponding argument is an lvalue of type A, hence neither is better than the other. Hence, the only opportunity that can determine which is better falls on [over.match.best#general-2.2]

the context is an initialization by user-defined conversion (see [dcl.init], [over.match.conv], and [over.match.ref]) and the standard conversion sequence from the return type of F1 to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type of F2 to the destination type.

The second standard conversion sequences of them are both identity conversions, hence they are not indistinguishable. So, the result is ambiguity. GCC is merely correct in that the program is ambiguous, but, obviously, its diagnosis has a bit misleading. Since the copy/move constructors are not viable functions in this case at all, how could they cause the ambiguity? If we suppress the production of the defaulted move constructor, GCC and Clang are both incorrect here, which is back to this question you have referred.

Templia answered 14/9, 2021 at 1:59 Comment(2)
Thanks. When you say that copy/move constructors are not viable, do you mean that they shall not be called (e.g. copy/move elision)? Because now all compilers call them: gcc.godbolt.org/z/oqGWPf17aCapillaceous
@Capillaceous No, overload resolution essentially comprises candidates->viable candidates-> the best candidate, the best candidate is called for that context.Templia

© 2022 - 2024 — McMap. All rights reserved.