Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

enable_if with copy/move assignment operator

I have a class in which I want to enable the copy/move assignment operators only if a type parameter to the class is nothrow copy/move constructible respectively. So I tries this:

#include <type_traits>

template<typename T>
struct Foobar {

    Foobar(T value) : x(value) {}
    Foobar(const Foobar &other) : x(other.x) {}
    Foobar(Foobar &&other) : x(std::move(other.x)) {}

    template<bool Condition = std::is_nothrow_copy_constructible<T>::value,
             typename = typename std::enable_if<Condition>::type>
    Foobar &operator=(const Foobar &rhs) {
        x = rhs.x;
        return *this;
    }

    template<bool Condition = std::is_nothrow_move_constructible<T>::value,
             typename = typename std::enable_if<Condition>::type>
    Foobar &operator=(Foobar &&rhs) {
        x = std::move(rhs.x);
        return *this;
    }

    T x;
};

int main() {
    Foobar<int> foo(10);
    Foobar<int> bar(20);

    foo = bar;
    foo.operator=(bar);

    return 0;
}

Now, Clang gives me the following error:

enable_if_test.cpp:31:9: error: object of type 'Foobar<int>' cannot be assigned because its copy assignment operator is implicitly
      deleted
    foo = bar;
        ^
enable_if_test.cpp:8:5: note: copy assignment operator is implicitly deleted because 'Foobar<int>' has a user-declared move
      constructor
    Foobar(Foobar &&other) : x(std::move(other.x)) {}
    ^
enable_if_test.cpp:32:9: error: call to deleted member function 'operator='
    foo.operator=(bar);
    ~~~~^~~~~~~~~
enable_if_test.cpp:4:8: note: candidate function (the implicit copy assignment operator) has been implicitly deleted
struct Foobar {
       ^
enable_if_test.cpp:12:13: note: candidate function [with Condition = true, $1 = void]
    Foobar &operator=(const Foobar &rhs) {
            ^
enable_if_test.cpp:19:13: note: candidate function [with Condition = true, $1 = void] not viable: no known conversion from
      'Foobar<int>' to 'Foobar<int> &&' for 1st argument
    Foobar &operator=(Foobar &&rhs) {
            ^
2 errors generated.

Now, I included the explicit call to the assignment operator to showcase the weirdness of the error. It just says that my template is a candidate function and gives no reason for why it is not chosen.

My guess here is that since I defined a move constructor, the compiler implicitly deleted the copy assignment operator. Since it is implicitly deleted, it doesn't even want to instantiate any template. Why it behaves like this I do not know, however.

How can I achieve this behavior?

(Note: The actual code has legitimate reasons to define each of the members, I'm aware that it is useless here.)

like image 334
Emil Eriksson Avatar asked Apr 16 '15 23:04

Emil Eriksson


People also ask

Can you use the copy constructor in the assignment operator?

You can safely invoke the copy assignment operator from the constructor as long as the operator is not declared virtual.

What does the move assignment operator do?

Move assignment operators typically "steal" the resources held by the argument (e.g. pointers to dynamically-allocated objects, file descriptors, TCP sockets, I/O streams, running threads, etc.), rather than make copies of them, and leave the argument in some valid but otherwise indeterminate state.

What is the difference between the Move and Copy operator?

Move constructor moves the resources in the heap, i.e., unlike copy constructors which copy the data of the existing object and assigning it to the new object move constructor just makes the pointer of the declared object to point to the data of temporary object and nulls out the pointer of the temporary objects.

What is the difference between assignment operator and copy methods with examples?

The difference between a copy constructor and an assignment operator is that a copy constructor helps to create a copy of an already existing object without altering the original value of the created object, whereas an assignment operator helps to assign a new value to a data member or an object in the program.


1 Answers

The best and only way to do this is via the rule of zero -- use the compiler-provided assignment operators and constructors, which copy or move each of the members. If the member T x cannot be copy (move) assigned, then the copy (move) assignment operator for your class will default to deleted.

The reason that SFINAE cannot be used to disable copy and/or move assignment operators is that SFINAE requires template context, but copy and move assignment operators are non-template member functions.

A user-declared copy assignment operator X::operator= is a non-static non-template member function of class X with exactly one parameter of type X, X&, const X&, volatile X& or const volatile X&.

Since your template versions don't count as user-declared copy (move) assignment operators, they won't inhibit generation of the default ones, and because non-templates are preferred, the default ones will be preferred over your template definitions (when the argument is a const Foobar&, otherwise the template is a better match, but disabling the template still won't disable the auto-generated one).

If you need some special logic in addition to the call to the member's copy (move) assignment operator, implement it in a subobject (base or member are both feasible).


You can perhaps accomplish your goal by selecting from specializations of a class template which you use as a base class, passing the appropriate type traits as you inherit:

template<bool allow_copy_assign, bool allow_move_assign>
struct AssignmentEnabler;

template<typename T>
struct Foobar : AssignmentEnabler<std::is_nothrow_copy_constructible<T>::value, 
                                  std::is_nothrow_move_constructible<T>::value>
{
};

The derived type will use the rule of zero to default to having copy and move assignment if and only if the selected AssignmentEnabler base class does. You must specialize AssignmentEnabler for each of the four combinations (neither copy nor move, copy without move, move without copy, both).

Complete conversion of the code in your question:

#include <type_traits>

template<bool enable>
struct CopyAssignmentEnabler {};

template<>
struct CopyAssignmentEnabler<false>
{
    CopyAssignmentEnabler() = default;
    CopyAssignmentEnabler(const CopyAssignmentEnabler&) = default;
    CopyAssignmentEnabler(CopyAssignmentEnabler&&) = default;
    CopyAssignmentEnabler& operator=(const CopyAssignmentEnabler&) = delete;
    CopyAssignmentEnabler& operator=(CopyAssignmentEnabler&&) = default;
};

template<bool enable>
struct MoveAssignmentEnabler {};

template<>
struct MoveAssignmentEnabler<false>
{
    MoveAssignmentEnabler() = default;
    MoveAssignmentEnabler(const MoveAssignmentEnabler&) = default;
    MoveAssignmentEnabler(MoveAssignmentEnabler&&) = default;
    MoveAssignmentEnabler& operator=(const MoveAssignmentEnabler&) = default;
    MoveAssignmentEnabler& operator=(MoveAssignmentEnabler&&) = delete;
};

template<typename T>
struct Foobar : CopyAssignmentEnabler<std::is_nothrow_copy_constructible<T>::value>,
                MoveAssignmentEnabler<std::is_nothrow_move_constructible<T>::value>
{
    Foobar(T value) : x(value) {}

    T x;
};

int main() {
    Foobar<int> foo(10);
    Foobar<int> bar(20);

    foo = bar;
    foo.operator=(bar);

    return 0;
}
like image 120
Ben Voigt Avatar answered Oct 17 '22 04:10

Ben Voigt