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

5

11

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.

Spicate answered 18/3, 2020 at 19:11 Comment(4)
Wouldn't the obvious simple solution that you've posted be actually good? It's still concise, does not introduce macros, and explicitly states what you want, what might be better from maintainability perspective.Lumpen
@AdamKotwasinski it is good, I agree, just not the best if you have many optionals to unwrap and want to simplify your code with regards to the * unwrappingSpicate
@Alex: "For example, the obvious simple solution" Doesn't that copy the object? Wouldn't using *result not be better on performance grounds, if MyType is of some size/complexity?Hepza
@NicolBolas yes. A better option would be auto& result = *resultOpt; as @Barry wrote.Spicate
S
13

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.

Standardize answered 18/3, 2020 at 19:51 Comment(4)
this macro is kind of hilarious.Disjunction
That if false else is quite funny. Ok, I'll use the *result option. But out of curiosity, what own problems does your macro have, besides possible name collisions?Spicate
@Standardize Maybe one could let the user provide the whole declaration for name (so replace auto& name by name in the macro). Then so stuff like int val = 0; if_let(val, get_maybe_int()) ; or if_let(arr[0], get_maybe_val()); is possible and one can also stay const- correct.Disjunction
@Standardize what are some of the problems you refer to when saying "has its own problems"? other than creating a new variable and having an extra ifMonika
M
0

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
});
Monika answered 4/1, 2022 at 7:7 Comment(3)
I think in this variant, safe unwrap by * in the if block would be possible; could you also write a variant with an else block (e.g. usable for error handling)?Inductor
Safe as much a macro can be safe. So not much at all.Panties
there's a few ways to go about it, make else another param for example #define unwrap_else(x, val, block, elseBlock) if (auto opt_##x = val) { auto &x = opt_##x; block } else { elseBlock }Monika
D
-1

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;
    }
}
Disjunction answered 18/3, 2020 at 19:47 Comment(1)
The implicit conversion isn't enough for all cases. E.g. if auto x = blub() returned opt<std::string> and you wanted x.size().Pluperfect
I
-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) {

}
Inductor answered 3/1, 2022 at 14:31 Comment(0)
M
-2

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.

Monika answered 3/1, 2022 at 10:49 Comment(3)
If the optional you pass to _unwrap is not engaged, then *val will not be a reference to an object. And you can't copy from that.Hepza
@NicolBolas considering this example with a disengaged optional: std::optional<int> a; if (unwrap(A, a)) { printf("%i", A); } else { printf("no"); } a is disengaged (if I'm not wrong) and it doesn't matter that *a has undefined behavior because it gets kicked to the else scope, I get it that you can still access the variable but that's something you can still do with normal disengaged optional c++ unwrapping.Monika
The pair contains a T, not a T&. Therefore, that T must be constructed. *val returns a T&, which is used to initialize the T in the pair. That T is therefore initialized by copying from *val's T&. Since *val doesn't refer to an object, you will be copying from something that doesn't exist.Hepza

© 2022 - 2024 — McMap. All rights reserved.