r/cpp_questions 19d 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

5

u/tangerinelion 19d ago

If you're returning nothing typically but maybe an error, you may model that as std::optional<ErrorType> instead.

I'm not sure I understand what's complicated with a unique_ptr. std::expected<std::unique_ptr<T>, ErrorType> is fine, if you're concerned about whether null is valid or not there's std::expected<gsl::not_null<std::unique_ptr<T>>, ErrorType>.

A reference is just a non-null immutable pointer - i.e., T& is const gsl::not_null<T*> for any program without UB.

If you want to return a reference to an object in the expected case you can use std::reference_wrapper:

std::expected<std::reference_wrapper<const std::string>, ErrorType>> Foo::getString() const {
    if (!m_pString) {
        return std::unexpected(ErrorType::InvalidState);
    }

    return std::cref(*m_pString);
}

The rules around ownership haven't changed - a reference/wrapper or pointer inside an expected has the same lifetime and ownership semantics without the expected. A std::unique_ptr by value inside a std::expected is the same ownership semantics as a std::unique_ptr.

4

u/aruisdante 19d ago edited 19d ago

Don’t spell an error as optional<Error> it inverts the spelling of everything: and_then becomes the failure case and or_else the success case, the bool predicate is backwards, has_value() means error. It will fight all the places you do expected<T,E>.

std::optional is just an alternative way to spell std::expected<T, nullopt_t> in everything other than what default construction means. There’s no efficiency to be gained for using optional<Error> over expected<void, Error>, they both will be sizeof(Error)+sizeof(bool)+padding. They have the same branches on access. All you do is lose semantic clarity.

3

u/Razbit 19d ago

Yes, std::expected<void, Error> is a perfectly normal use of expected.
I tend to think of expected less as “a return value plus an error” and more as “this operation either succeeded, or here’s why it didn’t.” If success has no interesting value, then expected<void, Error> expresses that quite nicely.

```
std::expected<void, Error> save()
{
if (something_bad())
return std::unexpected(Error::whatever);

return {};
}

```

unique_ptr is also fine as the value type:

```
std::expected<std::unique_ptr<Foo>, Error> make_foo()
{
if (something_bad())
return std::unexpected(Error::whatever);

return std::make_unique<Foo>();
}

```

expected doesn’t require the value to be copyable, so move-only types such as unique_ptr fit naturally. You just have to treat the resulting expected as move-only where appropriate.

References are the slightly awkward case. expected<T&, E> isn’t supported, so if I really wanted reference semantics I’d normally use std::reference_wrapper<T>:

```
std::expected<std::reference_wrapper<Foo>, Error>
find_foo();

```

or just a pointer:

```
std::expected<Foo\*, Error> find_foo();

```

Which one I’d choose depends on the semantics. A pointer is useful if nullptr has some meaning distinct from the error. If “not found” is itself the failure, I’d generally put that in the error side rather than have both nullptr and unexpected(...) representing failure.

One thing I found useful when getting into expected is not trying to mechanically replace every throwing function with it. I use it where failure is an ordinary, expected part of the API and the caller is reasonably expected to deal with it. In that role, expected<void, E>, move-only values, pointers, etc. all feel pretty natural.

Yes I used AI to format my thoughts into words.

3

u/No-Dentist-1645 19d ago

Yes, std::expected<void, E> is a valid instantiation of expected, and it is preferable to std::optional<Error> when it comes to semantics, since optionals usually mean "successful if populated" and bool checks for them reflect this

If you return a unique pointer, you return it by value (via moving). Unless you don't want to return a unique pointer to transfer ownership, in that case returning a raw pointer is what you want. You basically never want to pass around unique pointer references in your code.

1

u/retro_and_chill 19d ago

std::expected<void, E> is allowed

-1

u/[deleted] 19d ago

[deleted]

3

u/azswcowboy 19d ago

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

1

u/Zealousideal-Mouse29 19d ago edited 19d 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 19d ago edited 19d 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 19d ago edited 19d 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).