The C++ standard library template type std::
has one of two states. It could be empty (not contain anything), or it could contain a T
.
Suppose you start with an empty std::
. How do you put a T
into it?
One of my colleagues tried to do it in what seemed to be the most natural way: Use the assignment operator.
struct Doodad { Doodad(); ~Doodad(); std::unique_ptr<DoodadStuff> m_stuff; }; struct Widget { std::optional<Doodad> m_doodad; Widget() { if (doodads_enabled()) { // I guess we need a Doodad too. Doodad d; m_doodad = d; } } };
Unfortunately, the assignment failed to compile:
Widget.cpp: error C2679: binary '=': no operator found which takes a right-hand operand of type 'Doodad' (or there is no acceptable conversion)
I asked for the rest of the error message, because the details will explain what the compiler tried to do (and why it couldn’t). It’s long, but we’ll walk through it.
optional(617,1): could be 'std::optional<Doodad> &std::optional<Doodad>::operator =(const std::optional<Doodad> &)' Widget.cpp(100,9): 'std::optional<Doodad> &std::optional<Doodad>::operator =(const std::optional<Doodad> &)': cannot convert argument 2 from 'Doodad' to 'const std::optional<Doodad> &' Widget.cpp(100,27): Reason: cannot convert from 'Doodad' to 'const std::optional<Doodad>' Widget.cpp(100,27): No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called optional(283,28): or 'std::optional<Doodad> &std::optional<Doodad>::operator =(std::nullopt_t) noexcept' Widget.cpp(100,9): 'std::optional<Doodad> &std::optional<Doodad>::operator =(std::nullopt_t) noexcept': cannot convert argument 2 from 'Doodad' to 'std::nullopt_t' Widget.cpp(100,27): No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called optional(321,28): or 'std::optional<Doodad> &std::optional<Doodad>::operator =(std::optional<_Ty2> &&) noexcept(<expr>)' Widget.cpp(100,9): 'std::optional<Doodad> &std::optional<Doodad>::operator =(std::optional<_Ty2> &&) noexcept(<expr>)': could not deduce template argument for 'std::optional<_Ty2> &&' from 'Doodad' optional(307,28): or 'std::optional<Doodad> &std::optional<Doodad>::operator =(const std::optional<_Ty2> &) noexcept(<expr>)' Widget.cpp(100,9): 'std::optional<Doodad> &std::optional<Doodad>::operator =(const std::optional<_Ty2> &) noexcept(<expr>)': could not deduce template argument for 'const std::optional<_Ty2> &' from 'Doodad' optional(292,28): or 'std::optional<Doodad> &std::optional<Doodad>::operator =(_Ty2 &&) noexcept(<expr>)' Widget.cpp(100,9): 'std::optional<Doodad> &std::optional<Doodad>::operator =(_Ty2 &&) noexcept(<expr>)': could not deduce template argument for '__formal' optional(288,33): 'std::enable_if_t<false,int>' : Failed to specialize alias template Widget.cpp(100,9): while trying to match the argument list '(std::optional<Doodad>, Doodad)'
The compiler is showing its work. It’s showing you all the possible overloaded assignment operators and explained why each one failed. The way to understand what went wrong is to look for the overload you intended to use and see why the compiler rejected it. Let’s take them one at a time.
could be 'std::optional<Doodad> &std::optional<Doodad>::operator =(const std::optional<Doodad> &)' cannot convert argument 2 from 'Doodad' to 'const std::optional<Doodad> &' Reason: cannot convert from 'Doodad' to 'const std::optional<Doodad>' No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called
The first assignment operator available is the one where you assign a std::optional<Doodad>
to another std::optional<Doodad>
. This one failed because you passed a Doodad
, not a std::optional<Doodad>
, and there was no eligible conversion.
Okay, what’s next?
or 'std::optional<Doodad> &std::optional<Doodad>::operator =(std::nullopt_t) noexcept' 'std::optional<Doodad> &std::optional<Doodad>::operator =(std::nullopt_t) noexcept': cannot convert argument 2 from 'Doodad' to 'std::nullopt_t' No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called
This is the emptying assignment, where you can assign a std::nullopt
to the optional to return it to the empty state. This is not what we wanted either, so we’re not surprised that it failed.
Onward.
or 'std::optional<Doodad> &std::optional<Doodad>::operator =(std::optional<_Ty2> &&) noexcept(<expr>)' 'std::optional<Doodad> &std::optional<Doodad>::operator =(std::optional<_Ty2> &&) noexcept(<expr>)': could not deduce template argument for 'std::optional<_Ty2> &&' from 'Doodad'
This is the case of move-assigning a std::optional<T2>
to a std::optional<T1>
. This is also not what we were trying to do, so the fact that it failed is expected.
Keep going.
or 'std::optional<Doodad> &std::optional<Doodad>::operator =(const std::optional<_Ty2> &) noexcept(<expr>)' 'std::optional<Doodad> &std::optional<Doodad>::operator =(const std::optional<_Ty2> &) noexcept(<expr>)': could not deduce template argument for 'const std::optional<_Ty2> &' from 'Doodad'
This is the copy-assignment version of the above, so we can skip this one, too.
or 'std::optional<Doodad> &std::optional<Doodad>::operator =(_Ty2 &&) noexcept(<expr>)' 'std::optional<Doodad> &std::optional<Doodad>::operator =(_Ty2 &&) noexcept(<expr>)': could not deduce template argument for '__formal' 'std::enable_if_t<false,int>' : Failed to specialize alias template while trying to match the argument list '(std::optional<Doodad>, Doodad)'
This is the final catch-all case of assigning an arbitrary object to an optional. This is the one we were hoping to use, but somehow it failed because of a “could not deduce template argument” from std::
, and that leading false
tells us that an enable_if
precondition failed. Let’s look at the precondition.
template <class _Ty2 = _Ty, enable_if_t< conjunction_v< negation< is_same<optional, _Remove_cvref_t<_Ty2>> >, negation< conjunction<is_scalar<_Ty>, is_same<_Ty, decay_t<_Ty2>>> >, is_constructible<_Ty, _Ty2>, is_assignable<_Ty&, _Ty2> >, int> = 0> _CONSTEXPR20 optional& operator=(_Ty2&& _Right) noexcept(⟦...⟧)
Let’s work on simplifying this template metaprogramming. In our case, _Ty2
is Doodad&
, so std::
is std::
, which is Doodad
. From its name, it’s highly likely that the internal template _Remove_cvref_t
is std::
+std::
, but if you don’t trust your intuition, you can look it up for yourself:
template<class _Ty> using _Remove_Cvref_t _MSVC_KNOWN_SEMANTICS = remove_cv_t<remove_reference_t<_Ty>>;
Applying it to the case where _Ty2
is Doodad&
results in remove_cv_t<remove_reference_t<Doodad&>>
which is remove_cv_t<Doodad>
which is just Doodad
. Plugging all that back into the enable_if
, as well as _Ty = Doodad
(since _Ty
is the template parameter to optional
itself) gives us this:
enable_if_t< conjunction_v< negation< is_same<optional, Doodad> >, negation< conjunction<is_scalar<Doodad>, is_same<Doodad, Doodad>> >, is_constructible<Doodad, Doodad&>, is_assignable<Doodad&, Doodad&> >, int> = 0> _CONSTEXPR20 optional& operator=(Doodad;& _Right) noexcept(⟦...⟧)
Now we can interpret the expression. The operator is enabled if…
!is_same<optional, Doodad> && !(is_scalar<Doodad> && is_same<Doodad, Doodad>) && is_constructible<Doodad, Doodad&> && is_assignable<Doodad&, Doodad&>
(It so happens that these are precisely the conditions spelled out in the C++ language specification. I doubt this is a coincidence.)
The first clause says “you are not assigning from a std::optional<Doodad>
“, which is true. We are assigning from a Doodad
. The purpose of this clause is to remove this overload from consideration in favor of the other overload that specifically is for optional-to-optional assignment.
The second clause says “you are not trying to assign a scalar that is the same type of the optional.” I think this is to remove this overload from consideration in favor of converting the source scalar to an optional<_Ty>
and assigning that. Regardless, it doesn’t apply here, so we pass that test too.
The next test is to see whether a Doodad
can be constructed from a Doodad&
, and in the case of a Doodad
, it turns out that this is not true because the Doodad
contains a unique_ptr
, which makes it non-copyable.
Okay, so we can fix that by using std::move
to move the Doodad
on the stack into the optional, right?
Doodad d; m_doodad = std::move(d); // or even m_doodad = Doodad();
Unfortunately, this fails in basically the same way. But how can that be?
It’s because Doodad
is not move-assignable, even though all of its members are movable!
The requirements for an implicitly-defined move-assignment operator are that the type have no user-declared copy constructors, move constructors, copy assignment operators, or destructors. Our Doodad
has a destructor, so that removes the implicitly-defined move-assignment operator.
Bonus reading: Implicit Move Must Go.
So our Doodad
is not movable, not copyable.
One solution is to make our Doodad
movable. This means investigating the class invariants and verifying that memberwise std::move
preserves them. This can get tricky if, for example, the Doodad
allowed pointers to itself to escape. If you’ve done the analysis and confirmed that memberwise std::move
is correct behavior, you can add
Doodad(Doodad&&) = default; Doodad& operator=(Doodad&&) = default;
to ask for the compiler to generate a default move constructor and default move assignment operator.
But maybe you study the Doodad
and conclude that it is not movable for whatever reason. What else can you do?
We’ll look at our options next time.
Standard file streams are noncopyable but are movable, so they still have value semantics (which is usually what we care about). Examples of things that definitively shouldn't have value semantics at all (shouldn't be movable) include atomic variables, since the whole point of atomic variables is to have a stable memory address that threads can rendezvous at.
Edit: I realize you may be talking about the pre-C++0x world, in which case, yes, noncopyable classes couldn't have...
I’ve recently returned to C++ development, after a 20+ year detour in C#/Java land, and I find myself shocked by things like this. Who thinks this is a reasonable way for programmers to work — wading through dozens of lines of error messages, for something as simple as an assignment operation.. and we need a multi-part article from Raymond to explain how to do it?
We don’t need a multi-part article, this is just a contrived example that most C++ developers would not encounter because it’s unnatural to write this sort of code, and Raymond enjoys diving into the gory details of such incidents. That’s why we all enjoy reading this blog.
This wasn’t contrived. It actually happened. Though it has been simplified for expository purposes.
I don’t get why incoming values need to be movable or *anything* to be put to optional, optional should be just a wrapper and there shouldn’t be needed to construct or manipulate the inner object in anyway.
But maybe i’m writing something dumb with java colored glasses here
This is very much a case of Java colored glasses.
In Java: Objects exist on the heap, and variables point to them. When an object is no longer needed, the JVM destroys it (garbage collection). An instance of java.util.Optional contains a pointer to an object (which may be null), which means that the bytes that make up the object are at some random allocation on the heap, and have nothing to do with the bytes that...
The most common use case for optional is to write a function that *may* return a value. So it either returns empty optional or optional with some data. And commonly that data is going to be created inside that function, on stack. Any data located on stack must be returned from the function as a value (i.e.: copied or moved) because if you'd returned a reference, that data would cease to exist at the moment...
While I'd say it's more about either 1) novice C++ programmers blindly chanting magic (defining non-defaulted default ctor, dtor, etc) they heard from others but never understood the meanings (and dangers) of, or 2) the programmer genuinely believing in OOP supremacy and refusing to study value semantics.
If were written before C++0x, it would nevertheless not prevent you from assigning between values of the class if the programmer writing it knew value semantics and did...
Not everything makes sense to be copyable though, and trying to force that often results in you turning things into refcounted handles, which have their own undesirable gotchas. For example C++ standard file streams are not copyable but some operating systems do allow duplicating file handles, for better or for worse…
My favourite last resort for bad-behaving classes is to call the destructor, followed by in-place constructor. In this case the std::optional’s std::in_place_t constructor should do the trick.
Since you’re in Widget’s constructor, I feel it’s safe to
emplace
the Doodad directly. (This wouldn’t be true if the Doodad was already visible to outside code, as there might be extra initialisation to do.)Filed in category: things that C++ programmers think of as normal, but look completely beyond the pale bonkers to other programmers.
For what it’s worth, we will hit a similar problem if our Widget has a simple Doodad m_doodad; member and we want to change it.
It’s less “normal” and more “backward compatibility”. As explained in the bonus reading, if Doodad was allowed to be implicitly movable, that would break a lot of old pre-C++11 code that wasn’t designed with move semantics in mind. It sucks but this is the best solution: just require explicitly defaulting the move operations in this case.
It has already bypassed PL/I and Algol 68 famous complexity, setting a new standard.
Which is a pity, it is one of my favourite languages, however somehow what the language scientists design on their ISO lab, and what we foots on the ground wish for, went into split directions.