While answering another question, I noticed something peculiar about conversion operators when dealing with ref-qualifiers.
Consider the following code:
using P = std::unique_ptr<int>;
struct A {
P p;
operator P() && { return std::move(p); }
operator P const&() const& { return p; }
};
int main() {
A a;
P p;
p = std::move(a);
}
This does not compile because apparently there is ambiguity when selecting the correct operator overload (see the errors in the demo below). It works if I remove the const
qualifier on the second overload like this:
operator P const&() & { return p; }
Also, if instead of an assignment I simply construct a P
object, it also works:
P p = std::move(a);
However, this only happens for conversion operators. If instead I write a normal member function that does the exact same thing, it compiles just fine.
struct B {
P p;
P get() && { return std::move(p); }
P const& get() const& { return p; }
};
int main() {
B b;
P p;
p = std::move(b).get();
}
Why is that? What's so special about a conversion operator for these overloads to be ambiguous when they aren't on a normal member function?
Side note: if instead of std::unique_ptr<int>
, I use a custom non-copyable type, nothing changes.
struct P {
P() = default;
P(P const&) = delete;
P(P&&) = default;
P& operator=(P const&) = delete;
P& operator=(P&&) = default;
};
Other side note: for some reason, MSVC doesn't say there is an ambiguity, it just selects the wrong overload. Unless I use my custom non-copyable type, in which case it agrees the call is ambiguous. So I guess it has to do with std::unique_ptr::operator=
. Not too important, but if you have any idea why, I'd love to know.
When you write p = std::move(a)
, it is actually p.operator=(std::move(a))
. There are two relevant candidates for this function:
P& operator=(P&&) noexcept; // (1) Move assign operator
P& operator=(const P&); // (2) Copy assign operator
The fact that the second one is deleted isn't considered yet.
So, the conversion from a A
rvalue to something that P&&
would accept in (1) calls the user defined conversion function operator P() &&
.
For the second overload, it would call operator const P&() const&
.
Both of these options are user-defined conversion functions, so neither is better in terms of overload resolution, thus an ambiguity.
But if you remove the const
from operator const P&() /*const*/&
, it can no longer be called (since std::move(a)
isn't an lvalue, so it can't call an lvalue qualified function if they aren't const qualified), and there is no ambiguity since the other choice is removed.
You can try it yourself for a function not named operator=
:
struct P {
void f(const P&) = delete;
void f(P&&) {}
};
struct A {
P p;
operator P() && { return std::move(p); }
operator P const&() const& { return p; }
};
int main() {
A a;
P p;
p.f(std::move(a));
}
For the case of B
, std::move(b).get()
is either going to be of type P
or const P
. The overload resolution is done by the call to get()
, and an rvalue ref qualified function wins over a const lvalue ref qualified function for an rvalue, std::move(b).get()
is going to be an rvalue P
, and there are no ambiguities in choosing the move assign operator.
For P p = std::move(a);
, the overload resolution is a bit different: Since it is initializing a P
object, it's looking for the best way to convert from std::move(a)
.
The candidates are all the constructors of P
+ all the conversion functions of a
.
operator P() &&
beats operator P() const&
because the conversion being considered is from std::move(a)
to A&&
or const A&
to call the conversion operator, not to P&&
or const P&
when matching arguments of a constructor.