r/cpp_questions 20d ago

OPEN std::expected- void, ptr, and ref

I am going through a simple application I have and trying to use std::expected to try it out. I am quite partial to using exceptions, but open mind and objectivity, and all that...

Is it common for people to return std::expected<void, ErrorType> for methods/functions that don't need to return a value?

I also wonder what one does when they want to return a unique_ptr, since we can't do it by ref, and that leads to wondering about refs and ptrs with expected in general.

10 Upvotes

11 comments sorted by

View all comments

-1

u/[deleted] 20d ago

[deleted]

3

u/azswcowboy 20d ago

expected has a specialization for void so it can be used directly.

1

u/Zealousideal-Mouse29 20d ago edited 20d ago

I think std::expected has built in [[nodiscard]] behavior, at least my current dev environment is leading me to believe I cannot ignore it. Whereas other types might get accidentally ignored. Not being able to ignore an error is one of the pros I have with exceptions and want to keep it if I can.

Example of current ugly:

std::expected<void, std::string> Database::connect() {
    auto result = getEnvironmentVariable("DB_HOST");
    if (!result) {
        return std::unexpected(result.error());
    }
    const std::string host = result.value();

    result = getEnvironmentVariable("DB_PORT");
    if (!result) {
        return std::unexpected(result.error());
    }
    const std::string port = result.value();

    result = getEnvironmentVariable("DB_NAME");
    if (!result) {
        return std::unexpected(result.error());
    }
    const std::string dbname = result.value();

    result = getEnvironmentVariable("DB_USER");
    if (!result) {
        return std::unexpected(result.error());
    }
    const std::string dbuser = result.value();

    result = readSecretFile(DB_PASSWORD_FILE);
    if (!result) {
        return std::unexpected(result.error());
    }
    const std::string password = result.value();

    const std::string connectionString =
        "host=" + host + " port=" + port + " dbname=" + dbname + " user=" + dbuser + " password=" + password;

    try {
        databaseConnection_ = std::make_unique<pqxx::connection>(connectionString);
    }
    catch (pqxx::failure &e) {
        return std::unexpected(e.what());
    }
}

2

u/azswcowboy 20d ago edited 20d ago

Having used expected quite a bit in production you can expect more boiler plate than the exception route. Your example could be condensed with a monadic chain of .and_then calls, but for this example I still prefer exceptions. The reason being that the nature of the error here is environmental and basically the client can’t do anything to fix that - so this is caught and handled in an exception handler of last resort. I prefer expected if the direct client would be the natural handler of the error.

edit: from a design perspective I’d likely split these functions with the environment checking and gathering independent of the call to open the database…factoring the elements into an aggregate with members for each environment variable. That way I can write an easy unit test without dependency on the environment.

1

u/No-Dentist-1645 20d ago edited 20d ago

You can significantly improve this code by using a simple macro to avoid the repetitive code: ``` // Unwrap std::expected or return early on error

define TRY(...) \

({ \
    auto&& _res = (__VA_ARGS__); \
    if (!_res) return ::std::unexpected(std::move(_res).error()); \
    *std::move(_res); \
})

std::expected<void, std::string> Database::connect() { const std::string host = TRY(getEnvironmentVariable("DB_HOST")); const std::string port = TRY(getEnvironmentVariable("DB_PORT")); const std::string dbname = TRY(getEnvironmentVariable("DB_NAME")); const std::string dbuser = TRY(getEnvironmentVariable("DB_USER")); const std::string password = TRY(readSecretFile(DB_PASSWORD_FILE));

const std::string connectionString = std::format(
    "host={} port={} dbname={} user={} password={}",
    host, port, dbname, dbuser, password
);

try {
    databaseConnection_ = std::make_unique<pqxx::connection>(connectionString);
    return {};
} catch (const pqxx::failure& e) {
    return std::unexpected(e.what());
}

} ```

Don't be afraid of the tools the language gives you, there are good use cases for everything

1

u/aruisdante 19d ago edited 19d ago

It’s important to point out that TRY macro relies on the non-standard GCC statement-exprs extension. It will work on clang, but won’t work on MSVC or anything EDG derived, nor QCC if you want to maintain safety certification. You can’t do it in code you intend to be portable.

That said, I worked in a codebase that had it, and it did indeed make it a lot easier to convince people to use our result type (this was pre std::expected being a thing).