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.
CodePudding user response:
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.
