C++11 Templates – enable_if with Copy/Move Assignment Operator

assignment-operatorc++c++11templates

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.)

Best Answer

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;
}
Related Question