A function and its caller communicate with each other via two mechanisms: parameters and return values. When a function is called, the caller provides arguments, which the function receives via its parameters.
Parameters normally send information into a function. Return values send information back to the caller. Out parameters blur that direction: the caller passes a variable into the function, and the function writes a result back into it.
In parameters
An in parameter receives information from the caller.
#include <iostream>
void printSpeed(int speedRpm){
std::cout << "Speed: " << speedRpm << " rpm\n";
}
int main(){
printSpeed(1800);
}
// Terminal output:
Speed: 1800 rpm
The direction of data flow is clear. The function accepts anint and prints it out.
Out parameters
An out parameter is a parameter used to send information back to the caller.
This is usually done with a non-const lvalue reference or pointer.
Consider a function that splits a duration in seconds into minutes and seconds:
#include <iostream>
void splitDuration(int totalSeconds, int& minutesOut, int& secondsOut){
minutesOut = totalSeconds / 60;
secondsOut = totalSeconds % 60;
}
int main(){
int minutes {};
int seconds {};
splitDuration(367, minutes, seconds);
std::cout << minutes << " minutes, "
<< seconds << " seconds\n";
}
Terminal output:
// 6 minutes, 7 seconds
The function works, but the call site is not ideal:
splitDuration(367, minutes, seconds);
A reader has to inspect the function declaration to know that minutes and seconds will be modified.
The call itself does not make the output direction obvious.
That is the main problem with out parameters: they hide mutation behind ordinary-looking arguments.
Out parameters make call sites awkward
Out parameters force the caller to create variables before making the call.
int minutes {};
int seconds {};
splitDuration(367, minutes, seconds);
That is more ceremony than necessary.
If the function is meant to produce a result, returning the result is usually cleaner.
#include <iostream>
struct Duration {
int minutes {};
int seconds {};
};
Duration splitDuration(int totalSeconds){
return Duration {
.minutes = totalSeconds / 60,
.seconds = totalSeconds % 60
};
}
int main(){
Duration duration { splitDuration(367) };
std::cout << duration.minutes << " minutes, "
<< duration.seconds << " seconds\n";
}
// Terminal output:
6 minutes, 7 seconds
Now the direction of data flow is clear:
Duration duration { splitDuration(367) };
The function returns a Duration.
The caller receives a Duration.
No hidden mutation is required.
Returning a struct is often the better design
Out parameters are often used when a function needs to return more than one value.
That is not a strong reason anymore. C++ lets us return a struct directly.
For example, this is weak:
void readBattery(double& voltageOut, double& currentOut);
The caller must know which argument receives which result:
double voltage {};
double current {};
readBattery(voltage, current);
This is clearer:
struct BatteryReading {
double voltage {};
double current {};
};
BatteryReading readBattery();
The return type names the concept being returned.
Example:
#include <iostream>
struct BatteryReading {
double voltage {};
double current {};
};
BatteryReading readBattery(){
return BatteryReading {
.voltage = 12.4,
.current = 1.8
};
}
int main(){
BatteryReading reading { readBattery() };
std::cout << "Voltage: " << reading.voltage << " V\n";
std::cout << "Current: " << reading.current << " A\n";
}
// Terminal output:
Voltage: 12.4 V
Current: 1.8 A
This is easier to read because the returned object has a meaningful type.
Instead of pushing results into loose variables, the function returns one coherent result.
Return values compose better
Return values can be used immediately in expressions.
#include <iostream>
int getRetryLimit(){
return 3;
}
int main(){
std::cout << "Retries: " << getRetryLimit() << '\n';
int retries { getRetryLimit() };
std::cout << "Stored retries: " << retries << '\n';
}
// Terminal output:
Retries: 3
Stored retries: 3
The returned value can be printed, stored, passed to another function, or used in an expression.
Out parameters are less flexible:
#include <iostream>
void getRetryLimit(int& retriesOut){
retriesOut = 3;
}
int main(){
int retries {};
getRetryLimit(retries);
std::cout << "Retries: " << retries << '\n';
}
// Terminal output:
Retries: 3
This works, but it is more awkward.
The caller must create a variable first, pass it into the function, then use it afterward.
The return-by-value version is the more natural expression of the idea:
int retries { getRetryLimit() };
Conclusion
Ultimately, Avoid out-parameters (except in the rare case where no better options exist).
Prefer pass by reference for non-optional out-parameters or structs if multiple values must be returned.