Parameters passed by value, const reference, or pointer to const are sometimes called in parameters (short for input parameters) because such parameters are used to send values into the function (the function uses those values as inputs).
However, parameters passed by non-const reference allow a function to modify the value of the argument. If a function modifies the value of an argument, it is, in effect, outputting a value.
In-out parameters (short for input and output parameters) are parameters where the function both uses and changes the value of the parameter. The addOne
function we showed in a prior example has an in-out parameter
:
1 2 3 4 |
void addOne(int& ref) // ref is an in-out parameter { ++ref; // evaluates value of ref and changes it } |
Out parameters (short for output parameters) are parameters that the function only uses for outputting value. Out parameters
are sometimes labeled with an “out” or “Out” prefix or suffix to indicate their output-oriented nature. This can help remind the caller that the initial value passed to these parameters will be overwritten. By convention, out parameters
are typically the rightmost parameters.
Here’s an example of a program that uses output parameters
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> #include <cmath> // for std::sin() and std::cos() // degrees is an in parameter, sinOut and cosOut are out parameters void getSinCos(double degrees, double &sinOut, double &cosOut) { // std::sin() and std::cos() take radians, so we need to convert degrees to radians static constexpr double pi { 3.14159265358979323846 }; double radians = degrees * pi / 180.0; sinOut = std::sin(radians); cosOut = std::cos(radians); } int main() { // We need to define variables to hold our out parameters double sin{ 0.0 }; double cos{ 0.0 }; // getSinCos will "output" the sin and cos in variables sin and cos getSinCos(30.0, sin, cos); std::cout << "The sin is " << sin << '\n'; std::cout << "The cos is " << cos << '\n'; return 0; } |
This function takes one parameter (by value) as input, and “returns” two parameters (by reference) as output.
This is one way for a function to return more than one value.
The downsides using references for out and in-out parameters
While out
and in-out
parameters might seem like a convenient way to return multiple values, they have a few downsides. First, the caller must pass in arguments to hold the outputs even if the caller doesn’t intend to use them. More importantly, the syntax is a bit unnatural, with both the input and output parameters being mixed together in the function call.
This can make it easy to overlook that some arguments might be modified.
Some programmers and companies frown upon using non-const references for out
and in-out parameters
because the calling syntax offers no clue that the function will change the value. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> void doSomething(int mode) { // imagine code that does something useful with mode here } void doSomethingElse(int& mode) { // imagine code that does something useful with mode here mode = 2; } int main() { int mode { 1 }; doSomething(mode); // perfectly safe doSomethingElse(mode); // deceptively evil // Programmer still expects mode to be 1 // But doSomethingElse changed it to 2! if (mode == 1) std::cout << "No threat detected.\n"; else std::cout << "Launching nuclear missiles...\n"; return 0; } |
And we blew up the world again! We really need to kick this habit.
Note that there’s no easy way to tell by scanning the function calls that mode
is an in parameter
for function doSomething
and an in-out parameter
for function doSomethingElse
. They look identical. This makes out
and in-out parameters
passed by non-const reference potentially dangerous, because a value could be changed without the programmer realizing that was possible.
For advanced readers
This syntax issue can be addressed by using pass by address (using pointers) instead.
Instead of using an in-out parameter, it’s better to have an in-parameter and return the modified value using the function’s return value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <iostream> void addOneBad(int& ref) { ++ref; } int addOneGood(int ref) { return ++ref; } int main() { int value { 5 }; cout << "value = " << value << '\n'; addOneBad(value); // works but doesn't make clear that value is being modified cout << "value = " << value << '\n'; value = addOneGood(value); // works and makes clear that value is being modified cout << "value = " << value << '\n'; return 0; } |
This makes it clearer that value
is being modified.
Although out parameters
offer one way for a function to return multiple values, this use is generally frowned-upon, as there are better ways to return multiple values (e.g. structured binding).
Best practice
Avoid out parameters
(use other methods to return multiple values), and avoid in-out parameters
when practical.
Pass by address helps makes modifiable parameters explicit
In lesson The C++ Tutorial (Test Site) [1] we talked about how using references for in-out parameters can lead to problems recognizing that a function call may modify a value passed in:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> void doSomething(int mode) // this function does not modify the value of mode { // imagine code that does something useful with mode here } void doSomethingElse(int& mode) // this function modifies the value of mode { // imagine code that does something useful with mode here mode = 2; } int main() { int mode { 1 }; doSomething(mode); // perfectly safe doSomethingElse(mode); // deceptively evil // Programmer still expects mode to be 1 // But doSomethingElse changed it to 2! if (mode == 1) std::cout << "No threat detected.\n"; else std::cout << "Launching nuclear missiles...\n"; return 0; } |
Because of this, some companies (such as Google) have guidelines that state in-out parameters should be passed by address rather than by reference. Let’s update the program above to use pass by address:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> #include <cassert> void doSomething(int mode) // this function does not modify the value of mode { // imagine code that does something useful with mode here } void doSomethingElse(int *mode) // now pass by address instead of by reference { // imagine code that does something useful with mode here mode = 2; } int main() { int mode { 1 }; doSomething(mode); // perfectly safe doSomethingElse(&mode); // now it is clear that this function might modify argument 'mode' if (mode == 1) std::cout << "No threat detected.\n"; else std::cout << "Launching nuclear missiles...\n"; return 0; } |
Passing by address requires the caller to pass in a normal variable argument using the address-of operator (&), which helps identify that the function could modify the argument just by looking at function call.
This has a few downsides: first, the function now has to deal with null pointers, which means we’ve traded an unlikely problem (the function modifying a value without the programmer realizing it) for possible program crashes or asserts. Second, it doesn’t work for pointers, which don’t need the explicit ampersand to be passed by address, and still allow modification of the value being pointed to.
Given this, our recommendation is to hold the course on preferring pass by reference for in-out parameters.