out/inout parameters¶
One of the most important aspects of understanding the code behavior is to clearly see which values can change and at which point.
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¶
-
template<class T>
class out¶ Thin wrapper over a reference-like or a pointer-like type to specify an output-only function parameter.
It helps to simplify reasoning about the code behavior by making potential 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<class Range> void copy_range(out<Range&> dst, Range const& src) { ... } copy_range(out{x}, y);
Public Functions
-
template<class Arg>
inline explicit constexpr out(Arg &&arg)¶ Constructor from an arbitrary type convertible to the wrapped type.
-
template<class U>
inline constexpr out(out<U> &&src)¶ 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 T 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::optional
might not contain a value, but here it’s always available.
-
inline std::remove_reference_t<T> *operator->() noexcept¶
Provides direct access to the members of the wrapped type as
out->member
.Note
It would be better to support
out.member
syntax, but C++ doesn’t allow to overloadoperator.
yet.
-
template<class Src>
inline out &operator=(Src &&src)¶ operator= enables assignment extension by overloading the
assign
free function accessible via ADL.For example, if a third-party
Container
doesn’t support assignment from a range likewe can still achieve a similar syntaxdstContainer = srcRange;
by implementing theac::out{dstContainer} = srcRange;
assign
function in the same namespace as Container:template<class InRange> void assign(ac::out<Container&> dst, InRange const& src) { ... }
-
template<class Arg>
-
template<class T>
class inout : public ac::out<T>¶ Thin wrapper over a reference-like or a pointer-like type to specify an input-output function parameter.
It helps to simplify reasoning about the code behavior by making potential 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 clearly differentiated overloads, withsort_range(x);
[[nodiscard]]
as another measure to prevent incorrect use:template<class Range> void sort_range(ac::inout<Range&> range) { ... } template<class 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<class 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<class 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.
Design¶
out/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 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.