10

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.

Alex
  • 5,009
  • 3
  • 39
  • 73
  • 1
    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. – Adam Kotwasinski Mar 18 '20 at 19:23
  • 1
    @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 * unwrapping – Alex Mar 18 '20 at 19:24
  • @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? – Nicol Bolas Mar 18 '20 at 20:35
  • @NicolBolas yes. A better option would be `auto& result = *resultOpt;` as @Barry wrote. – Alex Mar 18 '20 at 20:53

5 Answers5

11

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    this macro is kind of hilarious. – n314159 Mar 18 '20 at 19:56
  • 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? – Alex Mar 18 '20 at 20:51
  • @Barry 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. – n314159 Mar 18 '20 at 21:57
  • @Barry 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 if – luis Jan 04 '22 at 02:40
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
});
luis
  • 2,067
  • 2
  • 13
  • 21
  • 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)? – Sebastian Jan 04 '22 at 07:58
  • Safe as much a macro can be safe. So not much at all. – Moia Jan 04 '22 at 08:35
  • 1
    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 }` – luis Jan 04 '22 at 08:35
-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;
    }
}
n314159
  • 4,990
  • 1
  • 5
  • 20
  • 1
    The implicit conversion isn't enough for all cases. E.g. if `auto x = blub()` returned `opt` and you wanted `x.size()`. – Zereges Mar 18 '20 at 19:57
-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) {

}
Sebastian
  • 1,834
  • 2
  • 10
  • 22
-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.

luis
  • 2,067
  • 2
  • 13
  • 21
  • 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. – Nicol Bolas Jan 04 '22 at 05:10
  • @NicolBolas considering this example with a disengaged optional: `std::optional 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. – luis Jan 04 '22 at 05:25
  • 1
    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. – Nicol Bolas Jan 04 '22 at 14:17