November 13th, 2024

The puzzle of trying to put an object into a std::optional

The C++ standard library template type std::optional<T> 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::optional<T>. 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::enable_if_t<false, int>, 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::decay_t<_Ty2> is std::decay_t<Doodad&>, which is Doodad. From its name, it’s highly likely that the internal template _Remove_cvref_t is std::remove_cv_t+std::remove_reference_t, 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.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

14 comments

Discussion is closed. Login to edit/delete existing comments.

  • 紅樓鍮 · Edited

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

    Read more
  • Shawn Van Ness

    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?

    • LB

      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.

      • Raymond ChenMicrosoft employee Author

        This wasn’t contrived. It actually happened. Though it has been simplified for expository purposes.

  • Sigge Mannen

    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

    • Kevin Norris · Edited

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

      Read more
    • Flynn Taggart

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

      Read more
  • 紅樓鍮 · Edited

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

    Read more
    • LB

      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…

  • IS4 · Edited

    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.

  • Neil Rashbrook

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

  • Roeland Schoukens

    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.

    • LB

      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.

    • Paulo Pinto · Edited

      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.