How to Deal with Values That Are Both Input and Output
Passing inputs and getting outputs from a function is pretty straightforward and uncontroversial:
- inputs get in as function arguments by const reference (or by value for primitive types),
- outputs get out via the return type.
Output function(Input1 const& input1, int input2);
Now this is all well, until input-output values get in the picture. An input-output value is a value that the function modifies.
One use case for that is with a value that goes through several functions that build it incrementally:
void addThis(Value& x); void addThat(Value& x); void topItOffWithACherry(Value& x);
This construction is not packed into a constructor of the type Value
because those functions can be called or not, in order to build the Value
with various combinations of features.
In the above snippet, input-output values are represented as non-const references. And this is the guideline provided by the C++ Core Guidelines: F.17: For “in-out” parameters, pass by reference to non-const.
But is it ok? Not everyone thinks so. Here are the views of several conference speakers on the question, with their alternative solutions.
Thanks a lot to Kate Gregory for reviewing of this article.
Kate Gregory: Find an abstraction
When I attended Kate Gregory’s talk at ACCU 2018 (which is a very good one btw), I was surprised by one of her guidelines. She recommends to avoid output parameters, which I totally understand (indeed outputs should come out via the return type). But she goes further than that, by suggesting that we should also avoid input-output parameters.
Parameters are fundamentally inputs of a function, they look like so at call sites, and it can be confusing to use a function parameter for output even if it’s also an input. It makes sense, but then how do we pass a parameter for a function to modify? There are valid use cases for this, isn’t there?
Along with the guideline of avoiding them, Kate gives a way out of input-output parameters.
In some cases, you can remove the input-output parameters altogether from a function, by transforming the function into a class method.
In our example, we could refactor the code so that it gets called this way:
x.addThis(); x.addThat(); x.topItOffWithACherry();
The implementation of the method goes and changes the value of the class data members, and we no longer have to deal with an (input-)output parameter.
What’s interesting is that when you read it, this code suggests that it modifies x
. On top of naming (that was already there) those methods now take void
and return void
. So apart from modifying the object they operate on, there is not much else they can do (apart from a global side-effect).
What if we can’t change the interface?
We don’t always have the possibility to modify the interface of Value
though. What if it is int
for example, or std::vector<std::string>
, a class from a third-party library or just some code that we don’t have ownership on?
In this case, Kate suggests to look for an abstraction. Let’s take the example of std::vector
to illustrate.
Say that we have a std::vector<Item>
, to which we’d like to add certain elements:
void addComplimentaryItem(std::vector<Item>& items);
We can’t modify the interface of std::vector
to add a method to add a complimentary item for a customer (and that’s probably a good thing that we can’t!).
One way that sometimes work is to take a step back and look at the context where this std::vector
is used. Maybe there is an abstraction it belongs to, for example an Order here.
When we find that abstraction, we can wrap our std::vector
in an Order class, that may also contain other things:
class Order { public: addComplimentaryItem(); // other things to do with an order... private: int orderId_; std::vector<Item> items_; };
And the input-output parameter is gone.
Don’t force an abstraction
This sort of refactoring is an improvement to the code, that goes beyond the removal of input-output parameters. Indeed, such an abstraction allows to tidy up some bits of code and to hide them behind a meaningful interface.
This is why we should do this sort of refactoring only when it leads to meaningful interfaces. It makes no sense to create a VectorWrapper
interface just for the sake of transforming the input-output parameters into class members.
Also, in the cases of function taking several input-output parameters, it can be harder to move the code towards one of them to create an abstraction.
Mathieu Ropert: carrying along the object’s guts
On his very well written blog, Mathieu demonstrates an experimental technique to get rid of input-output parameters: breaking them into an input parameter and an output parameter, and use move semantics:
Value x; x = addThis(std::move(x)); x = addThat(std::move(x)); x = topItOffWithACherry(std::move(x));
And the function would take the parameters by value:
Value addThis(Value x); Value addThat(Value x); Value topIfOffWithACherry(Value x);
An interesting advantage of using move semantics here is that it expresses that the input parameter plunges into the function and comes out of it via its return type.
And then there is std::swap
As a final note, consider the standard library function std::swap
, that takes no less than two input-output parameters:
template< typename T > void swap(T& a, T& b);
I don’t see a reasonable Swapper
abstraction that would get rid of the input-output parameters of std::swap
. And moving in and out the parameters to swap would be very confusing also. So none of the above techniques seems to work with std::swap
.
But on the other hand, std::swap
is… OK the way it is! Indeed, when you look at it from a call site:
std::swap(x, y);
it is unambiguous that it swaps together the contents of x
and y
.
But why is it OK? Is it because std::swap
does just that? Or is it because we’re used to it? Does everyone in the world like swap the way it is? Are there other cases where input-output parameters make the interface clear, or is std::swap
a singularity?
If you have an opinion about one of these question, we want to hear it! Please leave a comment below with your thoughts.
Don't want to miss out ? Follow:   Share this post!