4 ms·
The most common usage of out-parameters is when you want to return more than one thing from a function, in which case they can't be trivially converted to retur
by Denvercoder9 5y ago
The most common usage of out-parameters is when you want to return more than one thing from a function, in which case they can't be trivially converted to return values.
- ogogmad 5y agoSurely in C++ you can return a tuple. It's only in C you can't.
- huhtenberg 5y agoYou can return a struct in C, but that's as unconventional as it gets.
- Denvercoder9 5y agoSure, but that doesn't always help readability/clarity. Compare this: SomeType result; if (!do_something(&result)) return; process(result); with this: std::tuple<bool, SomeType> ret = do_something(); if (!std::get<0>(ret)) return; process(std::get<1>(ret)); You could improve it a bit by using `auto` or returning a struct with named members, but quite often I still find it less readable. Especially in situations that are more complex than this example, where e.g. you fallback to another function to fetch `result` if `do_something` failed.
- kaashif 5y agoIf there are only two status codes (success/failure) then it seems like optional would be more appropriate than a tuple of bool and SomeType. 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...
- PaulDavisThe1st 5y agofor performance-oriented code, the original example, where the argument is stack-allocated in the caller, passed by reference, and used based on the return value, has benefits that can't be replicated in the std::optional<> version. 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.
- Kranar 5y agoThere is no copy involved with the std::optional version.
- cyber_kinetist 5y agoThe thing is, are we sure? 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).
- Kranar 5y agoYes we are sure and it has nothing to do with optimizations, RVO, Godbolt or any of the completely irrelevant details you are talking about. The implementation of std::optional is not permitted by the standard to allocate memory.
- deleted 5y ago[deleted]
- Kranar 5y agoThere are numerous ways of handling this that are significantly better, for example using an std::optional yields this code: do_something().and_then(process); Or structured bindings: auto [error_code, result] = do_something(); if(error_code == 0) { process(result); }
- brandmeyer 5y agoIn the presence of types which are truthy (ie, contextually convertible to bool), its too easy to accidentally swap the unpacking: // Oops, backwards! auto [result, error_code] = do_something(); if(error_code) { process(result); }
- BoorishBears 5y agoTo me that's not easy... 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-implicit-bool-conversion.html https://clang.llvm.org/extra/clang-tidy/checks/readability-i...
- Kranar 5y agoFair enough, in my codebase we always order data in terms of dependencies so that independent data comes before dependent data. In this case the result depends on the error_code, and the error_code is independent, so the error_code must be listed first.
- BoorishBears 5y agoauto [error, value] = do_something() if(!error) process(value) Is there something wrong with this?
- kaashif 5y agoHmm, is it error, value or value, error? In this situation, I'd prefer something like std::optional or std::variant, which you can't get wrong as easily as swapping two variables in a structured bind. Those have their problems too, though, and I'd never claim std::variant is "nice" to use.
- jcranberry 5y agotuples are unfortunately terrible. Its almost always better to make another type with the fields you need than use a tuple.