Output Parameters
InOut Wrappers
Although the Google C++ Style Guide and consequently our style guide have a preference for return values over output parameters, there are still cases where having a function output data using a parameter is reasonable (e.g. when there are many outputs from a function and making a custom struct to return them all is inconvenient). Typically one could do this using pass-by-reference. Consider the following simple example:
#include <iostream>
void set_to_three(int &x) {
x = 3;
}
int main(int argc, char **argv) {
int val;
set_to_three(val);
std::cout << val << std::endl;
return 0;
}
This code as written works perfectly well. However, in practice such code can
be bugprone because developers cannot tell whether the call to set_to_three()
will modify its arguments. To improve code clarity, we therefore prefer the use
of our InOut
wrapper when passing in arguments that may be modified by a
function. Using it works like so:
void set_to_three(resim::InOut<int> x) {
*x = 3;
}
int main(int argc, char **argv) {
int val;
set_to_three(resim::InOut{val});
std::cout << val << std::endl;
return 0;
}
In this case, the reader immediately knows that val
may be changed by
set_to_three()
. Note that you can also use the arrow operator with InOut
:
#include <iostream>
#include "resim/utils/inout.hh"
struct Foo {
int x = 0;
};
void set_to_three(resim::InOut<Foo> f) {
f->x = 3;
}
int main(int argc, char **argv) {
Foo f;
set_to_three(resim::InOut{f});
std::cout << f.x << std::endl;
return 0;
}
Under the hood, InOut
is simply a pointer to the object passed in, so it
should be treated with care to avoid dangling references. It's only use is to
annotate the code so that users know which arguments they pass may be modified.
Typically, there is no danger of memory issues if, as in this example, we're
simply wrapping and passing a variable from the stack into our function.
NullableReference
Sometimes we only want a function to use an output parameter conditionally.
Normally, one could use a raw pointer to do this. If the pointer is not
nullptr
, the function populates it, but otherwise leaves it alone:
#include <iostream>
void maybe_set_to_three(int *x) {
std::cout << "Ran maybe_set_to_three()!" << std::endl;
if (x) {
*x = 3;
}
}
int main(int argc, char **argv) {
int val;
maybe_set_to_three(&val);
std::cout << val << std::endl;
maybe_set_to_three(nullptr);
return 0;
}
As above, however, we would like to make things more explicit. In this case, we
want to make it clear that the pointer is being used as a nullable reference
and that no memory ownership is being passed from the caller to the function.
To do this, we use the NullableReference
template:
#include <iostream>
#include "resim/utils/nullable_reference.hh"
void maybe_set_to_three(resim::NullableReference<int> x) {
std::cout << "Ran maybe_set_to_three()!" << std::endl;
if (x.has_value()) {
*x = 3;
}
}
int main(int argc, char **argv) {
int val;
maybe_set_to_three(resim::NullableReference{val});
std::cout << val << std::endl;
maybe_set_to_three(resim::null_reference);
return 0;
}
In some ways, this wrapper is almost the same as InOut
, and one could use
NullableReference
anywhere that an InOut
could be used. However, it is
important to have these as separate wrappers because a function can express
more about how it treats its parameters by its choice of InOut
(where its
clear that the function will output something there) or NullableReference
(where it's clear the function won't if you don't ask it to).
Note
Feel free to play around with the source code for the examples above.