optional<reference_wrapper<T>> vs. optional<T>& - practical examples?

Viewed 855

I have read about std::optional<std::reference_wrapper<T>> as a way to pass around optional references.

However, I'm unable to think of a practical example where I'd do that, instead of just using an optional<T>&.

For example, suppose I'm writing a function which needs to take an optional vector<int> by reference.

I could do this:

void f(optional<reference_wrapper<vector<int>>> v) {
    // ...
}

int main() {
    vector<int> v = {1, 2, 3, 4};
    f(make_optional<reference_wrapper<vector<int>>>(std::ref(v));
    return 0;
}

But why not just do this?

void f(optional<vector<int>>& v) {
    // ...
}

int main() {
    f(make_optional<vector<int>>>(std::initializer_list{1, 2, 3, 4}));
    return 0;
}

Please give an example where optional<reference_wrapper<T>> is preferable to optional<T>&. The semantic differences, and especially the ways they can be leveraged in practice aren't clear to me.

3 Answers

std::optional<T> & is a reference to an optional object that can own a T object. You can mutate a T (if one is contained) or you can clear the optional object that was passed in by reference, destroying the contained T.


std::optional<std::reference_wrapper<T>> is an optional object that can own a reference to a T, but it doesn't actually own the T itself. The T lives outside of the std::optional object. You can mutate the T (if a reference is contained) or you can clear the optional object, which does not destroy the T. You can also make the optional object point at a different T, but this would be kind of pointless since the caller is passing you an optional by value.

Note that we already have a type built-in to the language that means "optional reference to a T": T*. Both a raw pointer and an optional reference have basically the same semantics: you either get nothing or you get a handle to an object you don't own. In modern C++, a raw pointer is the way to express an optional value not owned by the receiver.

I can't think of a single reason I'd ever explicitly use std::optional<std::reference_wrapper<T>> instead of T*.

But why not just do this?

Because your code doesn't compile. make_optional returns a prvalue, and you cannot pass a prvalue to a function that takes a non-const lvalue reference.

That's important because it shows the fundamental difference between these two cases. If you already have a T or a reference to a T from somewhere else, then you cannot pass that to a function that takes an optional<T>&. You'd have to copy the T into an optional<T> variable, then pass a reference to the optional variable to the function.

You wouldn't be able to modify the outside world's T. And that's the difference: with reference_wrapper<T>, you could.

Or if you have a function that can work with or without a modifiable T, you could just pass a T* like most people would.

As the other answers state, the two types serve different purposes, as the reference is to different things (a reference to an optional in one case and a reference to a vector in the other). Rather than repeating the explanation, here is some code you can play with to see the functional differences.

#include <iostream>
#include <vector>
#include <functional>
#include <optional>


// For better readability:
using optional_reference_vector = std::optional<std::reference_wrapper<std::vector<int>>>;
using optional_vector           = std::optional<std::vector<int>>;


void f(optional_reference_vector v) {
    v->get().push_back(5);
}

void g(optional_vector & w) {
    w->push_back(5);
}

int main() {
    // Two identical vectors with which to work:
    std::vector<int> v = {1, 2, 3, 4};
    std::vector<int> w = {1, 2, 3, 4};

    // Demonstrate an optional reference to a vector
    // ---------------------------------------------
    // Create a reference to `v` in `opt_v`.
    // Changes to `opt_v` will be reflected in `v` (and vice versa).
    optional_reference_vector opt_v {std::ref(v)};
    v.clear();
    // A copy of `opt_v` will be made in f(). Since we are copying a reference to
    // a vector and not the vector itself, the vector in main() is changed by f().
    f(opt_v);
    // Both `v` and `opt_v` refer to the same vector, so the size is the same.
    std::cout << "Using a reference to the vector:\n"
              << "Original vector size: " << v.size() << '\n'
              << "Optional vector size: " << opt_v->get().size() << "\n\n";

    // Demonstrate a reference to an optional vector
    // ---------------------------------------------
    // Copy `w` into `opt_w`.
    // Changes to `opt_w` have no effect on `w` (and vice versa).
    optional_vector opt_w {w};
    w.clear();
    // A reference to `opt_w` will be used in g(), so `opt_w` is updated.
    g(opt_w);
    // There are two vectors that now have different sizes.
    std::cout << "Using a copy of the vector:\n"
              << "Original vector size: " << w.size() << '\n'
              << "Optional vector size: " << opt_w->size() << '\n';

    return 0;
}

The output from this code:

Using a reference to the vector:
Original vector size: 1
Optional vector size: 1

Using a copy of the vector:
Original vector size: 0
Optional vector size: 5
Related