In C++, use exchange or swap with nullopt to move out of std::optional

What to do

When you want to move out of C++ std::optional in an expression, prefer to std::exchange it with a std::nullopt:

std::optional<std::string> src = "hi";
auto dest = std::exchange(src, std::nullopt);

The source receives the nullopt state (so it no longer holds a value), and the expression evaluates to the optional’s former state. If the source was nullopt, then the expression evaluates to nullopt; if you are sure that the source is non-nullopt, then you can immediately use .value() or dereference * on the expression to obtain the underlying value.

(For readers familiar with Rust, this is analogous to Option::take.)

Using std::swap also works, but requires a separate statement and a previously declared swap destination that is “move-assignable”:

std::optional<std::string> swap_src = "hi";
std::optional<std::string> swap_dest;
std::swap(swap_src, swap_dest);

In some cases, swap may be more efficient, because it is more common for swap to be template-specialized than exchange, but the difference is unlikely to matter outside of hot loops. Consult Godbolt and a profiler if performance is an overriding concern; otherwise prefer the version that’s more readable.

What not to do: pitfalls of other approaches

It is tempting to only move the optional:

std::optional<std::string> moved_from_optional = "hi";
auto only_moved_to = std::move(moved_from_optional);
std::cout << "moved_from_optional: " << moved_from_optional.has_value() << std::endl;

but this leaves the original in the non-nullopt state (older C++ standards proposals call this the “engaged” state, but modern references don’t seem to use this terminology). The snippet above will print moved_from_optional: 1.

Leaving the moved-from optional with the hollowed-out, moved-from value is almost never useful. In the above example, moved_from_optional contains the empty string. It is almost certainly a programming defect to use this value in any way; you’re probably better off with an optional that returns false from .has_value() and throws on .value().

What if we only move the contained value, not the optional? The same result pertains:

std::optional<std::string> moved_from_value = "hi";
auto moved_value = std::move(moved_from_value.value());
std::cout << "moved_from_value: " << moved_from_value.has_value() << std::endl;

This will print moved_from_value: 1.

Note that there are multiple references out there suggesting that one of the above approaches is better than the other. This is wrong: for most practical purposes, they are not even meaningfully different, and neither is what you want in the common case. The only benefit, compared to doing exchange or swap with a nullopt, is that you might save an instruction clearing the discriminant field of the optional. In most cases, this is unlikely to matter, and may even be optimized away if the compiler can detect that the source optional is dead after the move.

One can make raw move less error-prone by following it with a separate reset call; the following call will print move_then_reset: 0:

std::optional<std::string> move_then_reset = "hi";
auto moved_to = std::move(move_then_reset);
move_then_reset.reset();
std::cout << "move_then_reset: " << move_then_reset.has_value() << std::endl;

This prints moved_then_reset: 0. However, this seems strictly worse than using std::exchange: it is more verbose and requires more diligence.

Postscript

A couple of closing thoughts:

First, as of writing, none of the top hits on Stack Overflow or Google currently recommend using std::exchange or std::swap for this task, even though they have been in the standard since C++14 and C++11 respectively. The actual best other source I can find is this answer on StackOverflow, which is not even the top voted response on its question. I don’t know what to make of this, other than to point out that if you’re doing engineering, then (a) don’t expect other people to do your work for you, and (b) recognize that our information ecosystem does not reliably rank the best information most highly.

Second, this post is one of just a handful of pieces of writing that I’ve posted to my extremely sparse homepage in the past few years. You might guess from this that I am some kind of C++ aficionado. In reality, I have written only tens of KLOC of C++ code in my entire life, and the necessity of mastering countless subtleties like the above to do mundane tasks well is a major reason that I dislike C++. Even “modern” C++ is a terrifying mountain of complexity and user-hostile design with minimal guardrails to prevent you from tumbling down, and it is not true, as is often claimed, that you can easily avoid the steep ledges by sticking to “modern” idioms. (For example, bonus fact about std::optional: the standard does not promise that dereferencing an unset (nullopt) optional fails in any well-defined way; it’s simply undefined behavior.) If the use of C++ for building important large-scale systems does not worry you, then you either don’t know enough C++ or you fundamentally misunderstand the stochastic nature of software engineering at scale.

Appendix: Complete demonstration code

Save this as optional.cpp and run g++ --std=c++17 optional.cpp && ./a.out:

#include <iostream>
#include <optional>
#include <utility>

template <typename T>
void print(const char *prefix, const std::optional<T> &opt) {
    std::cout << prefix;
    if (opt.has_value()) {
        std::cout << "some(" << *opt << ")";
    } else {
        std::cout << "none";
    }
    std::cout << std::endl;
}

int main() {
    std::optional<std::string> src = "hi";
    auto dest = std::exchange(src, std::nullopt);
    print("src: ", src);
    print("dest: ", dest);
    std::cout << std::endl;

    std::optional<std::string> swap_src = "hi";
    std::optional<std::string> swap_dest;
    std::swap(swap_src, swap_dest);
    print("swap_src: ", swap_src);
    print("swap_dest: ", swap_dest);
    std::cout << std::endl;

    std::optional<std::string> moved_from_optional = "hi";
    auto only_moved_to = std::move(moved_from_optional);
    print("moved_from_optional: ", moved_from_optional);
    print("only_moved_to: ", only_moved_to);
    std::cout << std::endl;

    std::optional<std::string> moved_from_value = "hi";
    auto moved_value = std::move(moved_from_value.value());
    print("moved_from_value: ", moved_from_value);
    std::cout << "moved_value (not an optional): " << moved_value << std::endl;
    std::cout << std::endl;

    std::optional<std::string> move_then_reset = "hi";
    auto moved_to = std::move(move_then_reset);
    move_then_reset.reset();
    print("move_then_reset: ", move_then_reset);
    print("moved_to: ", moved_to);

    return 0;
}

Output:

src: none
dest: some(hi)

swap_src: none
swap_dest: some(hi)

moved_from_optional: some()
only_moved_to: some(hi)

moved_from_value: some()
moved_value (not an optional): hi

move_then_reset: none
moved_to: some(hi)