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:

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

inline constexpr operator Ref() const noexcept

Implicit conversion to the wrapped type.

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.member syntax, but C++ doesn’t allow to overload operator. yet.

template<typename Source>
inline Ref operator=(Source &&source)

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

targetContainer = sourceRange;
we can still achieve a similar syntax
ac::out{targetContainer} = sourceRange;
by implementing the assign function in the same namespace as Container:
template<typename SourceRange>
void assign(ac::out<Container&> target, SourceRange const& source) {
    ...
}

Returns:

The wrapped reference, which is inconsistent with the convention to return *this from operator=. However, it’s intended to not return ac::out here, because each usage of a reference as an output parameter should be separately wrapped into ac::out.

#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:

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