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