This question is based on previous SO discussion (which was affected by non-compliant compilers). So I'm using the latest c++23 released versions of gcc/clang/MSVC.
Here is a simple test with conversion operator overloading, and passing it to a variant:
struct C
{
template <typename T> operator T () {return 0.5;}
operator int () {return 1;}
operator std::string () { return "";}
};
int main ()
{
C c;
std::cout << std::variant<int, double, std::string>{c}.index() << "\n"; // selects template
std::cout << std::variant<int, std::string, double>{c}.index() << "\n"; // selects template
// demand to use string, MSVC fails
std::cout << std::variant<int, std::string, double>{std::in_place_index<1>, c}.index() << "\n";
}
I expected either the code to fail, because of ambiguity, or select type int
if variant
does index deduction in declaration order. But it always selects the template operator, no matter the order the types are mentioned in variant
. Are compilers correct? What's the explanation? Or perhaps there an UB. Also is that a bug in MSVC failing to compile the last line?
You are expecting to call the constructor of the form
template< class T >
constexpr variant( T&& t ) noexcept(/*...*/);
to construct the std::variant
objects. But that is not happening. This constructor is not viable in any of the cases, because T
would be deduced to C
. But the constructor is excluded from overload resolution if it would be ambiguous which element type should be constructed from the T
, which is now C
. That ambiguity is exactly what is happening in your construction: Imaging an overloaded function F
with the overloads
void F(int);
void F(double);
void F(std::string);
calling F(std::forward<T>(t))
where T
is C
would be ill-formed because overload resolution is ambiguous as all three candidates are viable, but with different choices for the conversion function in the argument.
What happens instead is that you choose the move constructor of std::variant</*...*/>
. This constructor is viable, because the templated conversion function of C
can be used to convert c
to std::variant</*...*/>
by deducing T
to that type.
Then, inside the templated conversion function you copy-initialize the std::variant</*...*/>
instance returned by the conversion function from the operand of the return
statement. The type of the operand is double
, so the double
element type of the variant will be constructed.
MSVC is also correct to reject the last case: The constructor that you are attempting to use is excluded from overload resolution unless std::string
is constructible from c
, i.e. unless a declaration of the form
std::string _(c);
would be well-formed.
However, std::string
also has multiple constructors, for example:
basic_string( const basic_string& other );
basic_string( basic_string&& other ) noexcept;
explicit basic_string( const Allocator& alloc );
basic_string( const CharT* s, const Allocator& alloc = Allocator() );
basic_string( std::initializer_list<CharT> ilist,
const Allocator& alloc = Allocator() );
basic_string( std::nullptr_t ) = delete;
The first two are viable with your std::string
conversion function. The other ones are viable with your templated conversion function. Therefore, again, you have multiple overloads with conversion sequences that are considered indistinguishable and nothing else in the them making one better candidate than the other. The overload resolution for the construction would be ill-formed.
There is also one constructor for std::string
of the form
template< class StringViewLike >
explicit basic_string( const StringViewLike& t,
const Allocator& alloc = Allocator() );
This one would be preferred over the others if it is not excluded from overload resolution, because StringViewLike
can be deduced to C
, so that the conversion sequence will be the identity conversion sequence. The question is then, whether or not the constructor is excluded from the overload resolution. It participates in overload resolution only if std::is_convertible_v<const StringViewLike&, std::basic_string_view<CharT, Traits>>
is true and std::is_convertible_v<const StringViewLike&, const CharT*>
is false.
In your case the second condition is certainly true
, because the templated conversion operator (uniquely) achieves the conversion. Therefore this constructor does not participate and can't make the construction of the std::string
unambiguous.
GCC and Clang are technically wrong to not diagnose the ill-formed last call as such, however they have a good reason to differ from the standard's requirements.
In the declaration of the form
std::string _(c);
where c
can be converted to std::string
via conversion function there shouldn't be any reason to consider the constructors of std::string
, which would, even if the move constructor was chosen unambiguously, always cause a redundant move. Instead the conversion function can directly initialize _
without involvement of a move constructor.
This is the currently still open CWG issue 2327. GCC and Clang seem to implement a resolution of this issue that would prefer the conversion function over constructors.
I guess the take-away from this is to avoid conversion functions when possible, especially templated ones.