Tuesday, 3 September 2013

Lazy approach to assignment operators

Most of the time we don't have to worry about defining copy/move constructors and assignment operators -- the compiler happily generates them for us. Sometimes, however, we must do the dirty work ourselves and code them up manually, often together with the destructor. By hand crafting the assignment operators, we sometimes gain extra efficiency (e.g. std::vector re-using the memory in copy-assignment) but most of the time the code just ends up looking like a copy/paste job of destructor and copy/move constructor.

If we're not lazy and define a swap function, we can use the copy/swap idiom to get a free pass on the copy-assignment operator. Not so for the move-assignment. This post from 2009 provides an interesting trick to reuse destructor/copy constructor to implement the copy-assignment without the swap function. Here's the code provided by that post:

struct A {
    A ();
    A (const A &a);
    virtual A &operator= (const A &a);
    virtual ~A ();
};

A &A::operator= (const A &a) {
    if (this != &a) {
        this->A::~A(); // explicit non-virtual destructor
        new (this) A(a); // placement new
    }
    return *this;
}

The author does warn about the downsides of using this trick but I think the concerns are fairly minor. The technique can also be extended for the move-assignment operator:

template <typename T, typename U>
A &A::operator= (A &&a) {
    if (this != &a) {
        this->A::~A(); // explicit non-virtual destructor
        new (this) A(std::move(a)); // placement new
    }
    return *this;
}

And can be generalized into a utility function that can cover both of those uses. The function and its usage is shown below:

template <typename T, typename U>
T& assign(T* obj, U&& other) {
    if( obj != &other ) {
        obj->T::~T();
        new (static_cast<void*>(obj)) T(std::forward<U>(other));
    }
    return *obj;
}

struct A {
    A ();
    A (const A& a);
    A(A&& a);
    virtual ~A ();
    A& operator= (const A& a) {
        return assign(this, a);
    }
    A& operator=(A&& a) {

        return assign(this, std::move(a));
    }
};

One very nice side affect of this approach is that it becomes possible to create a copy/move assignment operators for those classes that can otherwise only be copy/move constructable. For example, consider:

struct X {
   int& i;
};

The compiler will generate a copy/move constructor pair but will delete the corresponding assignment operators. You'd be hard pressed to define them yourself as well. But destroy/construct trick allows us to side step such limitations!

Note that assign's second argument is a "universal reference" and will bind to anything. Thus the assign function can actually by used to implement any assignment operator (not just copy/move) as long as the corresponding constructor is available.

Now suppose that struct X is located in a third party library and you don't want to modify it to add the assignment operators. By defining a utility class assignable<T>, we can add the desired functionality externally:

template <typename T>
class assignable : public T {
public:
    using T::T;

    assignable(T const& other) :
        T(other) {
    }

    assignable(T&& other) :
        T(std::move(other)) {
    }

    template <typename U>
    assignable& operator=(U&& other) {
        return assign(this, std::forward<U>(other));
    }
};

// and usage:
struct X {
    X(int& ii) : i(ii) {}
    int& i;
};

int i = 1, j = 2;
assignable<X> x(i);
x = assignable<X>(j);

This approach makes me wonder if the language should support a way of auto generating the copy/move assignments not member-wise but from destructor and constructor. We could then opt-in to such goodness like this:


Foo& operator=(Foo&&) = default(via_constructor);
Foo& operator=(Foo const&) = default(via_constructor);

The code (along with a work around for compilers not supporting inheritable constructors) is available on GitHub.

No comments:

Post a Comment