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:

copy_range(x, y);
Using ac::out wrapper, we can make it obvious without any extra documentation:
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>& to std::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 where ac::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 in std::optional, because std::optional might not contain a value, but here it’s always available.

inline constexpr operator T() const noexcept

Implicit conversion to the wrapped type.

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 overload operator. 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 like

dstContainer = srcRange;
we can still achieve a similar syntax
ac::out{dstContainer} = srcRange;
by implementing the assign function in the same namespace as Container:
template<class InRange>
void assign(ac::out<Container&> dst, InRange const& src) { ... }

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:

sort_range(x);
Using ac::inout wrapper, we can provide two clearly differentiated overloads, with [[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.

Source code

Tests

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.