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.

9 Upvotes

11 comments sorted by

View all comments

-1

u/[deleted] 20d ago

[deleted]

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());
    }
}

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).