out and inout parameter wrappers¶
One of the most important aspects of understanding the code behavior is to clearly see which values can change and when exactly.
Modern C++ strongly encourages returning by value over output parameters.
Structured binding and types like std::optional are the signs of that.
However, output parameters are still useful in some cases,
and C++ provides no built-in mechanism to distinguish them.
For example, can you imagine a C++ newcomer to guess which parameter represents the output range of this algorithm?
std::ranges::transform(x, y, z);
We aim to make such code more self-documenting with the help of simple wrappers:
ac::ranges::transform(x, ac::out{y}, z);
Difference between out and inout parameters¶
ac::inout shouldn’t be used just because
we read something from the parameter.
It should be used only if we read the referenced data.
For example, std::span references a contiguous block of memory.
But in order to access this memory, we clearly need to read
.data() and .size() from the span.
It’s recommended to still use ac::out<std::span<T>>
if we don’t read to referenced memory itself.
Reference¶
#include <actl/functional/parameter/out.hpp>
-
template<Reference Ref>
class out¶ Thin wrapper over a Reference type to specify an output-only function parameter.
Improves code clarity by making variable modifications clearly visible at both the function definition and all the call sites. For example, in operations similar to copy it’s not immediately clear which of the arguments we’re copying to:
Using ac::out wrapper, we can make it obvious without any extra documentation:copy_range(x, y);
template<typename Range> void copy_range(out<Range&> target, Range const& source) { ... } copy_range(out{x}, y);
Public Functions
-
template<typename Arg>
inline explicit constexpr out(Arg &&arg)¶ Constructor from an arbitrary type convertible to the wrapped type.
-
template<typename Source>
inline constexpr out(out<Source> &&source)¶ Function arguments in C++ can undergo an implicit conversion to match the parameter types. For example,
std::vector<int>&tostd::span<int>.This implicit constructor allows to preserve this behavior without the extra manual conversion when ac::out wrapper is used if the wrapped types are implicitly convertible. For example,
ac::out<std::vector<int>&>argument can be passed whereac::out<std::span<int>>parameter is expected.
-
inline Ref operator*() const noexcept¶
Accesses the wrapped value.
Note
operator* is used for consistency with
std::optional. Simple operator like this is even more appropriate here than instd::optional, becausestd::optionalmight not contain a value, but here it’s always available.
-
inline std::remove_reference_t<Ref> *operator->() noexcept¶
Provides direct access to the members of the wrapped type as
out->member.Note
It would be better to support
out.membersyntax, but C++ doesn’t allow to overloadoperator.yet.
-
template<typename Source>
inline Ref operator=(Source &&source)¶ operator= enables assignment extension by overloading the
assignfree function accessible via ADL.For example, if a third-party
Containerdoesn’t support assignment from a range likewe can still achieve a similar syntaxtargetContainer = sourceRange;
by implementing theac::out{targetContainer} = sourceRange;
assignfunction in the same namespace asContainer:template<typename SourceRange> void assign(ac::out<Container&> target, SourceRange const& source) { ... }
-
template<typename Arg>
#include <actl/functional/parameter/inout.hpp>
-
template<Reference Ref>
class inout : public ac::out<Ref>¶ Thin wrapper over a Reference type to specify an input-output function parameter.
Improves code clarity by making variable modifications clearly visible at both the function definition and all the call sites. For example, in operations similar to sort it’s not immediately clear if we modify the argument in-place or return a new value:
Using ac::inout wrapper, we can provide two obviously differentiated overloads, withsort_range(x);
[[nodiscard]]as another measure to prevent incorrect use:template<typename Range> void sort_range(ac::inout<Range&> range) { ... } template<typename Range> [[nodiscard]] Range sort_range(Range const& range) { ... } sort_range(ac::inout{x}); auto y = sort_range(x);
Interface of ac::inout basically repeats ac::out.
Detection traits¶
-
template<typename T>
bool ac::is_out_v = detail::is_out<std::remove_cvref_t<T>>::value¶ Checks whether T is a (possibly cvref-qualified) ac::out wrapper.
Note
If the program adds specializations for ac::is_inout_v, the behavior is undefined.
-
template<typename T>
bool ac::is_inout_v = detail::is_inout<std::remove_cvref_t<T>>::value¶ Checks whether T is a (possibly cvref-qualified) ac::inout wrapper.
Note
If the program adds specializations for ac::is_inout_v, the behavior is undefined.
See tests at tests/functional/parameter/out_inout.cpp
Design¶
out and inout wrappers are designed to be as simple as possible,
so they don’t enforce any guarantees about the parameters
being actually written to.
There are more advanced designs available with such guarantees,
for example the one from this article about the
output parameter.
But the added benefits don’t seem to be worth the extra complexity.
Especially considering that returning by value should be strongly preferred
over output parameters, so the latter are left to handle not a single object,
but ranges and the like.
Google C++ style guide recognized the problem of output parameters, but the proposed solution to use pointers was clearly misguided:
“In fact it is a very strong convention in Google code that input arguments are values or const references while output arguments are pointers.”
After many years this paragraph is finally removed from the guide. But there’s no alternative provided, which we’re addressing here.