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 actuallyp.operator=(std::move(a)). There are two relevant candidates for this function:The fact that the second one is deleted isn't considered yet.
So, the conversion from a
Arvalue to something thatP&&would accept in (1) calls the user defined conversion functionoperator 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
constfromoperator const P&() /*const*/&, it can no longer be called (sincestd::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=:For the case of
B,std::move(b).get()is either going to be of typePorconst P. The overload resolution is done by the call toget(), 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 rvalueP, 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 aPobject, it's looking for the best way to convert fromstd::move(a).The candidates are all the constructors of
P+ all the conversion functions ofa.operator P() &&beatsoperator P() const&because the conversion being considered is fromstd::move(a)toA&&orconst A&to call the conversion operator, not toP&&orconst P&when matching arguments of a constructor.