How to Make a Copyable Object Assignable in C++
Some types in C++ have a copy constructor that doesn’t have the same semantics as their assignment operator (operator=
).
Take references, for example. References can be copied:
int i = 42; int& r1 = i; int& r2 = r1; // r2 now points to i, like r1
But it doesn’t do the same thing as assigning to them:
int i1 = 42; int& r1 = i1; int i2 = 43; int& r2 = i2; r2 = r1; // r2 still points to i2
With the copy, r2
points to the same thing as r1
, but with the assignment r2
still points to the same object it was pointing to before.
Or take the example of copying a lambda:
auto lambda1 = [i](){ std::cout << i << '\n'; }; auto lambda2 = lambda1;
The above code compiles fine.
Now if we add the following line:
lambda2 = lambda1;
It doesn’t compile. As the compiler (clang) says:
error: object of type '(lambda at main.cpp:6:16)' cannot be assigned because its copy assignment operator is implicitly deleted
Lambdas don’t even have an operator=
to begin with (except in C++20 where they do if they don’t capture anything).
Right. But is any of this a problem?
Why we need operator=
After all, the behaviour of the references makes some sense, and why on earth would we like to assign on a lambda we’ve just created?
However, there is a case when the absence of operator=
becomes a problem: when the object that doesn’t have an operator=
is a member of a class. It makes it difficult for that class to have an operator=
itself. For one thing, the compiler is not going to write it for you.
Even for references, the compiler won’t generate an operator=
for a class if one of its members is a reference. It assumes that you’d better write it yourself to choose what to do with the reference member.
This problem came up in a project I’ve been working on, the pipes library. This library has classes that have lambdas as data members, and passes objects of those classes as output iterators of STL algorithms. And in Visual Studio, the STL in debug mode calls the operator=
on output iterators in the _Recheck
function. So the class that contains a lambda needs an operator=
.
Haven’t you ever faced too the situation where the compiler couldn’t write the operator=
you needed because of a problematic data member?
The standard has us covered for references
In C++11, and equivalently in Boost long before that, std::reference_wrapper<T>
has the same behaviour as a reference (you initialize it with a reference, and it even has a operator T&
) with one exception: it has an operator=
that rebinds the reference.
This means that after calling operator=
between two std::reference_wrapper
s, they point to the same object:
#include <functional> // ... int i1 = 42; auto r1 = std::ref(i1); // std::ref creates a std::reference_wrapper int i2 = 43; auto r2 = std::ref(i2); r2 = r1; // r2 now points to the i1, like r1
The fact that std::reference_wrapper<T>
has an operator=
allows the compiler to generate an operator=
for the classes that contains it. And the fact that it rebinds gives the operator=
of the containing class a natural behaviour.
Why is this behaviour natural? Because it is consistent with the copy of the reference: in both case, the two reference(_wrapper)s point to the same object after the operation.
The general case
Even if the case of references is solved with std::reference_wrapper
, the case of the lambda remains unsolved, along with all the types that have a copy constructor and no operator=
.
Let’s design a component, inspired from std::reference_wrapper
, that would add to any type an operator=
which is consistent with its copy constructor.
If you have an idea on how to name this component, just leave a comment below at the bottom of the post. For the time being, let’s call it assignable
.
template<typename T> class assignable {
assignable
needs an operator=
that relies on the copy constructor of its underlying type. Fortunately, we know how to implement that with a std::optional
, like we saw in How to Implement operator= When a Data Member Is a Lambda:
public: assignable& operator=(assignable const& other) { value_.emplace(*other.value_); return *this; } //... private: optional<T> value_;
But now that we’ve written the copy assignment operator, the compiler is going to refrain from generating the move constructor and the move assignment operator. It’s a shame, so let’s add them back:
assignable& operator=(assignable&& other) = default; assignable(assignable&& other) = default;
Now that we’ve written all this, we might as well write the copy constructor too. The compiler would have generated it for us, but I think it looks odd to write everything except this one:
assignable(assignable const& other) = default;
Finally, in order to hide from its users the fact that assignable
contains an optional
, let’s add constructors that accepts a T
:
assignable(T const& value) : value_(value) {} assignable(T&& value) : value_(std::move(value)) {}
Giving access to the underlying value
Like optional
, assignable
wraps a type to add an extra feature, but its goal is not to mimic the interface of the underlying object. So we should give access to the underlying object of assignable
. We will define a get()
member function, because operator*
and operator->
could suggest that there is an indirection (like for pointers and iterators).
The underlying object of the assignable
happens to to be the underlying object of the optional
inside of the assignable
:
T const& get() const { return value_; } T& get() { return value_; }
We don’t check for the nullity of the optional, because the interface of assignable
is such that all the paths leading to those dereferencing operators guarantee that the optional has been initialized.
Which gives us food for thought: optional
is not the optimal solution here. It contains a piece of information that we never use: whether the optional is null or not.
A better solution would be to create a component that does placement news like optional, but without the possibility to be null.
Lets keep this as food for thought for the moment. Maybe we’ll come back to it in a later article. Please leave a comment if you have thoughts on that.
Making the assignable
callable
std::reference_wrapper
has a little known feature that we explored in How to Pass a Polymorphic Object to an STL Algorithm: it has an operator()
that calls its underlying reference when it’s callable.
This is all the more relevant for assignable
since our motivating case was a lambda.
If we don’t implement operator()
, we’d have to write code like this:
(*assignableLambda)(arg1, arg2, arg3);
Whereas with an operator()
, calling code becomes more natural, resembling the one of a lambda:
assignableLambda(arg1, arg2, arg3);
Let’s do it then!
template<typename... Args> decltype(auto) operator()(Args&&... args) { return (*value_)(std::forward<Args>(args)...); }
We rely on C++14 decltype(auto)
. Note that we could also implement this in C++11 the following way:
template<typename... Args> auto operator()(Args&&... args) -> decltype((*value_)(std::forward<Args>(args)...)) { return (*value_)(std::forward<Args>(args)...); }
The case of assignable references
Now we have implemented an assignable<T>
that works when T
is a lambda.
But what if T
is a reference?
It can happen in the case of a function reference. In that case, we need exactly the same features as the ones we needed with the lambda.
However, assignable<T>
doesn’t even compile when T
is a reference. Why? Because it uses an std::optional<T>
and optional references didn’t make it in the C++ standard.
Luckily, implementing assignable
for references is not difficult. In fact, it’s a problem already solved by… std::reference_wrapper
!
So we need to create a specialization of assignable<T>
when T
is a reference. It would be great if we could just write this:
template<typename T> class assignable<T&> = std::reference_wrapper<T>; // imaginary C++
But this is not possible in C++.
Instead we have to implement a type that wraps std::reference_wrapper
and relies on its behaviour:
template<typename T> class assignable<T&> { public: explicit assignable(T& value) : value_(value) {} T const& get() const { return value_; } T& get() { return value_; } template<typename... Args> decltype(auto) operator()(Args&&... args) { return value_(std::forward<Args>(args)...); } private: std::reference_wrapper<T> value_; };
This way, we can use assignable
on reference types.
Putting it all together
In summary, here is all the code of assignable
all put together:
template<typename T> class assignable { public: assignable& operator=(assignable const& other) { value_.emplace(*other.value_); return *this; } assignable& operator=(assignable&& other) = default; assignable(assignable&& other) = default; assignable(assignable const& other) = default; assignable(T const& value) : value_(value) {} assignable(T&& value) : value_(std::move(value)) {} T const& get() const { return value_; } T& get() { return value_; } template<typename... Args> decltype(auto) operator()(Args&&... args) { return (*value_)(std::forward<Args>(args)...); } private: optional<T> value_; }; template<typename T> class assignable<T&> { public: explicit assignable(T& value) : value_(value) {} T const& get() const { return value_; } T& get() { return value_; } template<typename... Args> decltype(auto) operator()(Args&&... args) { return value_(std::forward<Args>(args)...); } private: std::reference_wrapper<T> value_; };
And classes can use it as a data member this way:
template<typename Function> class MyClass { public: // ... private: assignable<Function> myFunction_; };
For such as class, the compiler would be able to generate a operator=
as long as Function
has a copy constructor, which many classes–including lambdas–do.
Thanks to Eric Niebler for the inspiration, as assignable
was inspired from techniques I’ve seen in range-v3, which is my go-to model for library implementation.
If you have any piece of feedback on assignable
, I’d love to hear it in a comment below!
You will also like
- How to Pass a Polymorphic Object to an STL Algorithm
- How to Implement operator= When a Data Member Is a Lambda
- An Alternative Design to Iterators and Ranges, Using std::optional
- Why Optional References Didn’t Make It In C++17
- Pointers, References and Optional References in C++
- Smart Output Iterators: A Symmetrical Approach to Range Adaptors
Share this post!