const std::optional<SomeType> result = do_something();
if (!result.has_value()) {
return;
}
process(result.value());
This is nicer than the output parameter in my opinion, because it may be unclear what the meaning of a default constructed SomeType is. Default values are sometimes dangerous if the default is a real, valid input - errors and invalid states can end up being passed through.C++ doesn't have great support for sum types, so this is all always going to be pretty non-ergonomic, and the compiler doesn't always warn if you forget to do things. I might even say that the main advantage of using sum types is gone in C++ - you actually can forget to check has_value and call operator* on an empty optional.
It would be so easy for someone to make a mistake and write:
process(*do_something());
And invoke undefined behaviour. Whereas processing a default-constructed SomeType is at least not undefined behaviour (depending on what SomeType is, and what process does...).This whole thing is a mess, I hate C++, thinking about UB all the time...
I like the std::optional<> version and use that pattern some of the time. But if the object being passed/returned to/from do_something() is non-trivial, i don't want the copy overhead.
Can you go into detail? What are the benefits that can't be replicated?
If anything, the std::optional version seems better, since we can avoid an invocation of the SomeType constructor. Otherwise, everything is the same - everything's allocated on the stack, there is no dynamic allocation.
> If an optional<T> contains a value, the value is guaranteed to be allocated as part of the optional object footprint, i.e. no dynamic memory allocation ever takes place. Thus, an optional object models an object, not a pointer, even though operator*() and operator->() are defined.
That's from here: https://en.cppreference.com/w/cpp/utility/optional
Since C++17, RVO is required by the standard too:
> Return value optimization is mandatory and no longer considered as copy elision; see above.
From here: https://en.cppreference.com/w/cpp/language/copy_elision
I admit I'm not really a C++ expert, so maybe I'm missing something.
Unless your compiler is truly ancient odds are good it RVOs just fine no?
Probably many C++ aficionados will run to godbolt.org to prove this in a simple testcase, and they will probably be right. But then maybe it’s because you’ve only tested this for primitive or POD types. Maybe this might not be guaranteed for non-POD types with custom constructors? Or maybe this might be okay because of RVO or something? Hmm, let us read the specs again…
The problem is, C++ is a language that is very unintuitive about how your code is going to get mapped to actual hardware (assembly code). For many systems developers, it isn’t enough that the compiler might optimize this smartly; we instead want predictable compiler behavior. So if you want to make sure, and you’re not confident about the gnarly details of the C++ specification, it will be better for you just use out parameters instead of std::optional, if you are in the situation where you really need to squeeze out performance (which happens a lot for low-level systems programming).
The implementation of std::optional is not permitted by the standard to allocate memory.
It’s more specific to the example code the original commenter posted (and about general out-parameter usage) than the flaws of std::optional itself. Maybe this didn’t get communicated properly from my comment.
do_something().and_then(process);
Or structured bindings: auto [error_code, result] = do_something();
if(error_code == 0) {
process(result);
} // Oops, backwards!
auto [result, error_code] = do_something();
if(error_code) {
process(result);
}It's a very well established pattern to have (error, result) I'd immediately see something off with (result, error)
And clang-tidy can catch this: https://clang.llvm.org/extra/clang-tidy/checks/readability-i...
auto [error, value] = do_something()
if(!error)
process(value)
Is there something wrong with this?Those have their problems too, though, and I'd never claim std::variant is "nice" to use.