Search code examples
c++delete-operator

Why is a class with copy constructor and deleted move constructor not convertible to itself?


The queries "is_convertible_v move constructor deleted" and "c++ deleted move constructor makes class not convertible to itself", so I hope I'm not duplicating something thats already been answered... here goes...

I have the following class:

#include <type_traits>

struct OnlyCopyable {
    OnlyCopyable()  { }
    ~OnlyCopyable()  { }
    OnlyCopyable(const OnlyCopyable &src) { }
    OnlyCopyable& operator=(const OnlyCopyable &other) { return *this; }
    OnlyCopyable(OnlyCopyable &&other) = delete;
    OnlyCopyable& operator=(OnlyCopyable &&other) = delete;

};

static_assert(std::is_convertible_v<OnlyCopyable, OnlyCopyable>);

It appears to not be convertible to itself. Why is this?

If I comment out the lines explicitly deleting the move operators it becomes convertible. Why is that?

CPPReference has this to say:

If the imaginary function definition To test() { return std::declval<From>(); } is well-formed, (that is, either std::declval<From>() can be converted to To using implicit conversions, or both From and To are possibly cv-qualified void), provides the member constant value equal to true

Which I read as meaning that a class with a copy constructor only should be convertible to itself... what did I miss? Thanks.

EDIT:

I investigated a bit further and defined, as per the above quote, the following:

OnlyCopyable test() { return std::declval<OnlyCopyable>(); }

And low and behold that doesn't work with error message error: use of deleted function 'OnlyCopyable::OnlyCopyable(OnlyCopyable&&)'.

I can see why the above failed, because a deleted operator still takes part of overload resolution, and if chosen causes a fail.

This still puzzles me though... why is convertibility defined in this way? Surely the class is still convertible... just via copy and not move?


Solution

  • This still puzzles me though... why is convertibility defined in this way? Surely the class is still convertible... just via copy and not move?

    I'd argue that it's nonsensical1 for a class to be copyable but not movable, so this isn't a case that the standard should care about. The test "To test() { return std::declval<From>(); } is well formed" is simple, and that simplicity is valuable.

    rvalues are more general than lvalues, because of lvalue-to-rvalue conversion.

    If you don't define a move constructor, then overload resolution selects the copy constructor as the best viable function. Instead you have = deleted it, which leaves it in overload resolution, and results in an error when it is selected as the best viable function.

    1. You can always synthesise a move from a copy, because the parameter becomes an lvalue in the body. This does nothing compared to not declaring move, as rvalues bind to const lvalue references anyway.
    struct ExplicitMove {
        ExplicitMove(const ExplicitMove &src);
        ExplicitMove& operator=(const ExplicitMove &other);
        ExplicitMove(ExplicitMove &&other) : ExplicitMove(other) {}
        ExplicitMove& operator=(ExplicitMove &&other) { return *this = other; }
    
        // Other members...
    };