C++ Logo

std-proposals

Advanced search

[std-proposals] Fwd: On ergonomics of std::optional::or_else

From: Maxim Yanchenko <maxim.yanchenko_at_[hidden]>
Date: Tue, 16 Jul 2024 00:30:53 +0800
Hi everyone,

While working with std::optional in production code, I found out that one
usually needs to do something additional when the optional returned from
some API has no value (e.g. to throw an exception, to log an error, to log
that we're going to use a default).
While or_else looks like an ideal candidate to put those actions into, it
can be quite verbose because the callable must return the same optional
type - so you must mention the whole type either as a trailing return type
of the lambda, or in the return statement itself.

Assuming we have a function
std::optional<std::string> loadString();

we need to write either explicit trailing return type:

std::optional<std::string> f_throw() {
    return loadString().or_else([]() *-> std::optional<std::string>* {
        throw std::runtime_error("[FATAL] Failed to get value");
    });
}

std::optional<std::string> f_log() {
    return loadString().or_else([]() *-> std::optional<std::string>* {
        std::cout << "[ERROR] Failed to get value";
        *return std::nullopt;*
    });
}

std::optional<std::string> f_default() {
    return loadString().or_else([]() *-> std::optional<std::string>* {
        std::cout << "[WARN] Failed to get value, using default";
        return "default";
    });
}

or explicit type in the return statement:

std::optional<std::string> f_throw() {
    return loadString().or_else([] {
        throw std::runtime_error("[FATAL] Failed to get value");
        *return std::optional<std::string>{};*
    });
}

std::optional<std::string> f_log() {
    return loadString().or_else([] {
        std::cout << "[ERROR] Failed to get value";
        *return std::optional<std::string>{};*
    });
}

std::optional<std::string> f_default() {
    return loadString().or_else([] {
        std::cout << "[WARN] Failed to get value, using default";
        return *std::optional<std::string>*{"default"};
    });
}

I believe it would improve the usability of or_else if we allow the
function to return void (treated as nullopt) or a type that can be used to
construct the value type of the optional.
This would allow us to write these 3 functions shorter and cleaner, with
all the noise in bold removed:
std::optional<std::string> f_throw() {
    return loadString().or_else([] {
        throw std::runtime_error("[FATAL] Failed to get value");
    });
}

std::optional<std::string> f_log() {
    return loadString().or_else([] {
        std::cout << "[ERROR] Failed to get value";
    });
}

std::optional<std::string> f_default() {
    return loadString().or_else([] {
        std::cout << "[WARN] Failed to get value, using default";
        return "default";
    });
}

The implementation can be like this:
template <class T>
class std::optional<T> {
...
    template <class F>
    optional or_else(F&& f) {
        using R = std::remove_cvref_t<std::invoke_result_t<F>>;
        if constexpr (std::is_same_v<R, optional>) { // the current API
            return std::invoke(std::forward<F>(f));
        } else if constexpr (std::is_same_v<R, void>) { // void: return
std::nullopt;
            std::invoke(std::forward<F>(f));
            return std::nullopt;
        } else if constexpr (std::is_constructible_v<T, R>) { //
direct-construct
            return optional{std::invoke(std::forward<F>(f))};
        } else {
            static_assert(!std::is_same_v<F, F>, "Wrong return type");
        }
    }
};

Proof of concept: https://godbolt.org/z/W51bWbKo3

Would there be any interest in putting this into a proposal? Was something
like this discussed already at the committee meetings related to
std::optional?
Please let me know what you think.

Thanks,
Maxim

Received on 2024-07-15 16:31:36