How best to test and unwrap std::optional in an if statement

Viewed 3208

I have multiple functions that return a std::optional<T>. Here's an example for a made-up type MyType:

struct MyType {
    // ...
}

std::optional<MyType> calculateOptional() {
    // ... lengthy calculation

    if (success) {
        return MyType(/* etc */);
    }

    return std::nullopt;
}

Let's assume these functions are costly to run and I want to avoid calling them more than once.

When calling them I want to immediately test the optional, and if it does contain a value, I want to use it immediately and never again. In Swift, for example, I can use the standard if-let statement:

if let result = calculateOptional() {
    // Use result var
}

I would like to replicate this test-and-unwrap behavior in C++, while keeping the code as clean as possible at the point of use. For example, the obvious simple solution (to me at least) would be:

if (auto result = calculateOptional()) {
    MyType result_unwrapped = *result;
    // Use result_unwrapped var
}

But you have to unwrap inside the if, or use *result everywhere, which you don't have to do with Swift.

My only solution so far that genuinely gets close to the look and feel of Swift is:

template<typename T> bool optionalTestUnwrap(std::optional<T> opt, T& value) {
    if (!opt.has_value()) { return false; }
    value = *opt;
    return true;
}

#define ifopt(var, opt) if (typename decltype((opt))::value_type (var); optionalTestUnwrap((opt), (var)))

ifopt (result, calculateOptional()) {
    // Use result var
}

...but I'm also not a big fan of the use of a macro to replace a normal if statement.

5 Answers

Personally, I would just do:

if (auto result = calculateOptional()) {
    // use *result
}

with a second best of giving the optional an ugly name and making a nicer-named alias for it:

if (auto resultOpt = calculateOptional()) {
    auto& result = *resultOpt;
    // use result
}

I think this is good enough. It's a great use-case for intentionally shadowing an outer-scope name (i.e. naming both the optional and the inner alias result), but I don't think we need to go crazy here. Even using *result isn't a big problem - the type system will likely catch all misuses.


If we really want to go in on Swift, the macro you're using requires default construction - and it's not really necessary. We can do a little bit better with (ideally __opt is replaced by a mechanism that selects a unique name, e.g. concatenating with __LINE__):

#define if_let(name, expr)              \
    if (auto __opt = expr)              \
        if (auto& name = *__opt; false) {} else

As in:

if_let(result, calculateOptional()) {
    // use result
} else {
    // we didn't get a result
}

This doesn't have any extra overhead or requirements. But it's kind of ridiculous, has its own problems, and doesn't seem worthwhile. But if we're just having fun, this works.

Another simple and potentially safer one:

#define unwrap(x, val, block) if (auto opt_##x = val) { auto &x = opt_##x; block }

Usage:

unwrap(result, calculateOptional(), {
  // use result
});

You could wrap the optional in an own type with implicit conversion to the type and explicit to bool. Sorry I haven't tested this so far but I think it should work.

template<class T>
struct opt {
    std::optional<T> _optional; // public so it stays an aggregate, probably writing constructors is better

    explicit bool() const {
        return _optional.has_value();
    }

    T&() {
        return *_optional;
    }

    const T&() const {
         return *_optional;
    }

    T&&() && { // Let's be fancy
         return std::move(*optional);
    }
}

opt<int> blub(bool val) {
    return val ? opt<int>{0} : opt<int>{std::nullopt};
}

int main() {
    if(auto x = blub(val)) { // I hope this works as I think it does
        int y = x+1;
    }
}

If calculateOptional() returns a std::pair<bool sucess, T result> or can be converted in one, you can use the following construct:

if (auto [s, result] = calculatePair(); s) {

} else {

}

or you use exceptions; (...) catches all exceptions

try {
    auto result = calculate();

} catch (...) {

}

but you can be more specific

try {
    auto result = calculate();

} catch (nosuccess) {

}

This could be a clean way, inspired by all other answers in this post:

template <typename T>
inline std::pair<bool, T> _unwrap(const std::optional<T> &val) {
    return { val.has_value(), *val };
}

#define unwrap(x, val) const auto &[_##x, x] = _unwrap(val); (_##x)

Usage:

if (unwrap(result, calculateOptional())) {
  // result is now equivalent to *calculateOptional()
}

Pros:

  • You don't mess with the if statement
  • It maintains a method-like feel to it
  • You can still add more conditions to the right of the if statement

Cons:

  • Read-only but then again optionals already are

Happy to hear of any issues/fixes you guys might think there might be with this solution.

Related