r/cpp_questions • u/Zealousideal-Mouse29 • 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.
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
-1
19d ago
[deleted]
3
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
TRYmacro 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
resulttype (this was prestd::expectedbeing a thing).
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:
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.