Should templates make non-Rvalue-reference constructors/assigns for move only parameters of different type?

StackOverflow https://stackoverflow.com/questions/19746474

Question

Let us say I have a move-only type. We stop the default-provided constructors from existing, but Rvalue references introduce a new "flavor" we can use for the moving-versions of the signatures:

class CantCopyMe
{
private:
    CantCopyMe (CantCopyMe const & other) = delete;
    CantCopyMe & operator= (CantCopyMe const & other) = delete;

public:
    CantCopyMe (CantCopyMe && other) {
        /* ... */
    }

    CantCopyMe & operator= (CantCopyMe && other) {
        /* ... */
    }
};

I recently thought you were always supposed to pass movable types by Rvalue reference. Now it's looking like only very special cases need to do that...like these two. Things had seemed to work most of the time if you put them everywhere, but I just found one case of the compiler not running the part of the code transferring ownership.

(It was a situation like passing a unique pointer held in a variable with std::move to something taking a unique_ptr<foo> && parameter...but noticing the variable at the callsite hadn't been nulled. Changing the parameter to unique_ptr<foo> fixed it and it was properly nulled, thus preventing a double-delete. :-/ I haven't isolated why this one was bad when it seemed to work elsewhere, but a smoking gun is it worked the first time but not the subsequent calls.)

I'm sure there's a good reason for that, and many of you can saliently sum it up. In the meantime I've started going around like a good cargo-cult programmer removing the &&s.

But what if you're writing a templated class, where it looked like this?

template <class FooType>
class CantCopyMe
{
private:
    CantCopyMe (CantCopyMe const & other) = delete;
    CantCopyMe & operator= (CantCopyMe const & other) = delete;

public:
    template<class OtherFooType>
    CantCopyMe (CantCopyMe<OtherFooType> && other) {
        /* ... */
    }

    template<class OtherFooType>
    CantCopyMe & operator= (CantCopyMe<OtherFooType> && other) {
        /* ... */
    }
};

Is that bad practice for some reason, and you should break out separately when OtherFooType and FooType aren't the same... then it just passes by value?

template <class FooType>
class CantCopyMe
{
private:
    CantCopyMe (CantCopyMe const & other) = delete;
    CantCopyMe & operator= (CantCopyMe const & other) = delete;

public:
    CantCopyMe (CantCopyMe && other) {
        /* ... */
    }

    CantCopyMe & operator= (CantCopyMe && other) {
        /* ... */
    }

    template<class OtherFooType>
    CantCopyMe (CantCopyMe<OtherFooType> other) {
        /* ... */
    }

    template<class OtherFooType>
    CantCopyMe & operator= (CantCopyMe<OtherFooType> other) {
        /* ... */
    }
};
Was it helpful?

Solution

I think there's a simple answer for a possibly unexpected reason:

A copy-/move constructor or assignment-operator is never a template (specialization). E.g. [class.copy]/2

A non-template constructor for class X is a copy constructor if its first parameter is of type X&, const X&, volatile X& or const volatile X&, and either there are no other parameters or else all other parameters have default arguments.

Also, footnote 122 says:

Because a template assignment operator or an assignment operator taking an rvalue reference parameter is never a copy assignment operator, the presence of such an assignment operator does not suppress the implicit declaration of a copy assignment operator. Such assignment operators participate in overload resolution with other assignment operators, including copy assignment operators, and, if selected, will be used to assign an object.

Example:

#include <iostream>
#include <utility>

template<class T>
struct X
{
    X() {}

    template<class U>
    X(X<U>&&)
    {
        std::cout << "template \"move\" ctor\n";
    }

    template<class U>
    X& operator= (X<U>&&)
    {
        std::cout << "template \"move\" assignment-op\n";
        return *this;
    }
};

int main()
{
    X<int> x;                     // no output
    X<int> y(x);                  // no output
    y = std::move(x);             // no output
    X<double> z( std::move(x) );  // output
    y = std::move(z);             // output
}

In this example, the implicitly-declared move constructor and move assignment-operator are used.


Therefore, if you don't declare a non-template move ctor and move assignment-operator, they might be declared implicitly. They're not declared implicitly e.g. for the move assignment-op, if you have a user-declared dtor; for details see [class.copy]/11 and [class.copy]/20.

Example: Adding a dtor to the example above:

#include <iostream>
#include <utility>

template<class T>
struct X
{
    X() {}
    ~X() {}

    template<class U>
    X(X<U>&&)
    {
        std::cout << "template \"move\" ctor\n";
    }

    template<class U>
    X& operator= (X<U>&&)
    {
        std::cout << "template \"move\" assignment-op\n";
        return *this;
    }
};

int main()
{
    X<int> x;                     // no output
    X<int> y(x);                  // no output
    y = std::move(x);             // output
    X<double> z( std::move(x) );  // output
    y = std::move(z);             // output
}

Here, the first move-assignment y = std::move(x); calls a specialization of the assignment-operator template, because there's no implicitly declared move assignment-operator.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top