Search code examples
c++casting

Why do I get conversion is ambiguous error?


Consider the following piece of code:

#include <utility>
#include <iostream>

class Class
{
    int x;

public:
    explicit operator int()
    {
        std::cout << __func__ << "\n";
        return 1;
    }

    explicit operator int&()
    {
        std::cout << __func__ << "\n";
        return x;
    }

    explicit operator int&&()
    {
        std::cout << __func__ << "\n";
        return std::move(x);
    }
};

void foo(int)
{
    return;
}

void bar(int&)
{
    return;
}

void baz(int&&)
{
    return;
}

Class test()
{
    return {};
}

int main()
{
    Class obj;
    foo(static_cast<int>(obj)); // <---- THE ERROR IS ON THIS LINE
    bar(static_cast<int&>(obj));
    baz(static_cast<int&&>(std::move(obj)));
}

When I run this code, I get this error:

<source>: In function 'int main()':
<source>:51:26: error: conversion from 'Class' to 'int' is ambiguous
   51 |     foo(static_cast<int>(obj));
      |                          ^~~
<source>:51:26: note: there are 3 candidates
<source>:9:14: note: candidate 1: 'Class::operator int()'
    9 |     explicit operator int()
      |              ^~~~~~~~
<source>:15:14: note: candidate 2: 'Class::operator int&()'
   15 |     explicit operator int&()
      |              ^~~~~~~~
<source>:21:14: note: candidate 3: 'Class::operator int&&()'
   21 |     explicit operator int&&()
      |              ^~~~~~~~
Compiler returned: 1

I don't understand why I'm getting this error. Since I'm explicitly casting obj to int, and all the three conversion operators are marked as explicit, why is the compiler getting confused here?

I'm using gcc trunk on compiler explorer and compiling with -Ofast -std=c++23 -Wconversion -Wall -Wextra -Wpedantic -fanalyzer.

Here's the link to Godbolt.

Interestingly, compiling with clang results in even more errors. Here's the link to Godbolt.

<source>:51:9: error: ambiguous conversion for static_cast from 'Class' to 'int'
   51 |     foo(static_cast<int>(obj));
      |         ^~~~~~~~~~~~~~~~~~~~~
<source>:9:14: note: candidate function
    9 |     explicit operator int()
      |              ^
<source>:15:14: note: candidate function
   15 |     explicit operator int&()
      |              ^
<source>:21:14: note: candidate function
   21 |     explicit operator int&&()
      |              ^
<source>:53:9: error: reference initialization of type 'int &&' with initializer of type 'typename std::remove_reference<Class &>::type' (aka 'Class') is ambiguous
   53 |     baz(static_cast<int&&>(std::move(obj)));
      |         ^                  ~~~~~~~~~~~~~~
<source>:9:14: note: candidate function
    9 |     explicit operator int()
      |              ^
<source>:21:14: note: candidate function
   21 |     explicit operator int&&()
      |              ^
2 errors generated.

Do I have undefined behavior in my code?


Solution

  • The expression static_cast<int>(obj) has the same semantics as a direct-initialization

    int x(obj);
    

    The rules of the language compute a set of "permissible types" for explicit conversion functions, and then the conversion functions that satisfy this criterion are submitted to overload resolution. According to [over.match.conv]/1.1,

    [...] For direct-initialization, the permissible types for explicit conversion functions are those that can be converted to type T with a (possibly trivial) qualification conversion ([conv.qual]); otherwise there are none.

    T in this case is int, so the permissible types are cv int only. According to [over.match.funcs.general]/7:

    [...] If initializing an object, for any permissible type cv U, any cv2 U, cv2 U&, or cv2 U&& is also a permissible type.

    So, in fact, the permissible types are cv int, cv int&, and cv int&& (note that the cv pertains to the int, not to the reference). That makes operator int, operator int&, and operator int&& all candidates.

    Why is it like that? Because, when you call an operator int, operator int&, or operator int&&, in all cases the result of that call has type int; but the first one gives you a prvalue, the second an lvalue, and the third an xvalue. But since they all have type int, they're all equally good when the destination type is int.

    Note that the situation is different when you are initializing a reference. Initializing int& prefers a conversion function with return type int&. Initializing int&& prefers a conversion function with return type int or int&& (we do not discriminate between prvalues and xvalues), but can fall back on calling a conversion function with return type int&, making a copy, and then binding to that copy.