C++ / FUNCTIONS
Parameters, arguments, and return values
Distinguish parameters from arguments, predict how values convert as they cross the call boundary, and return exactly one value the caller can use.
What you will learn
- Read a parameter as a local variable that starts life holding the argument's value
- Bind arguments to parameters by position, never by matching names
- Spot the silent conversion when an argument or returned expression has the wrong type
- Return one value per call, and pack several into a struct when the caller needs more
Understanding Parameters, arguments, and return values
A parameter is a variable declared in the function's parameter list; an argument is the expression you supply at the call site. When the call happens each parameter is created and initialized from its matching argument, exactly as if you had written int left = right; at the top of the body. The match is made by position, so the names on the two sides are completely independent: a parameter called left will happily receive a caller's variable called right. Inside the body a parameter is just another local variable, so you can read it, assign to it, and it ceases to exist when the function returns.
An argument does not have to have the parameter's type, only to be convertible to it, because that initialization is an ordinary implicit conversion. That is why passing 2.9 to an int parameter quietly gives you 2 instead of an error, and why passing an int to a double parameter gives you 2.0. Reading the parameter list as a list of initializations tells you what will happen to each argument before the body ever runs.
The return type is the other half of the contract: it fixes the type of the whole call expression, no matter what type the expression after return has. A function declared int whose body says return n / 2.0; computes 3.5 and then converts it to 3, because return initializes an object of the declared return type. A function hands back at most one value, so group several results into a struct when you need more, and make sure every path out of a non-void function supplies that value: falling off the end is undefined behaviour, with main as the single exception.
<iostream>
// left and right are parameters: local variables of difference
int difference(int left, int right) {
return left - right;
}
// the declared return type, not the type of the expression, reaches the caller
int halve(int n) {
return n / 2.0;
}
int main() {
int right = 10;
int left = 4;
// binding is by position: left gets 10, right gets 4
std::cout << difference(right, left) << '\n';
// an argument can be any expression of a convertible type
std::cout << difference(1 + 1, 5) << '\n';
std::cout << halve(7) << '\n';
difference(9, 9); // the returned value may simply be discarded
}An argument initializes a parameter and return initializes the call's value, and in both cases the declaration, not the caller's variable or the returned expression, decides the type.
Worked examples
Returning nothing at all
Shows what return means in a void function and why such a call cannot be used as a value.
<iostream>
void print_positive(int n) {
if (n <= 0) {
std::cout << n << " is not positive\n";
return; // ends the call, no value attached
}
std::cout << n << " is positive\n";
}
int main() {
print_positive(5);
print_positive(-2);
std::cout << "done\n";
}Example explained
Line 1void says the function hands nothing back, so return carries no operand and writing return 0; here would not compile.
Line 2print_positive(-2) takes the if branch and returns immediately, so the "is positive" line below it is never reached.
Line 3Reaching the closing brace of a void function is equivalent to return;, which is why the last call needs no return statement.
Line 4Because there is no return value, int x = print_positive(5); is rejected: printing a value is not the same as returning it.
Two results in one return
Demonstrates the one-value-per-call rule and the usual way around it.
<iostream>
struct DivResult {
int quotient;
int remainder;
};
DivResult divide(int numerator, int denominator) {
return {numerator / denominator, numerator % denominator};
}
int main() {
auto [q, r] = divide(17, 5);
std::cout << "17 / 5 = " << q << " remainder " << r << '\n';
std::cout << divide(9, 4).remainder << '\n';
}Example explained
Line 1return {a, b}; initializes one DivResult from a braced list, so the function still returns a single value.
Line 2auto [q, r] is a C++17 structured binding that unpacks that one returned object into two named variables.
Line 3divide(9, 4).remainder reads a member of the returned temporary, which is destroyed at the end of that statement.
Feeding one call's result into another
Shows that a parameter is a private copy and that a return value can serve directly as an argument.
<iostream>
<string>
int digit_sum(int n) {
int sum = 0;
while (n > 0) {
sum += n % 10;
n /= 10;
}
return sum;
}
std::string label(int value) {
return "sum=" + std::to_string(value);
}
int main() {
std::cout << label(digit_sum(9876)) << '\n';
int n = 45;
std::cout << digit_sum(n) << ' ' << n << '\n';
}Example explained
Line 1The parameter n inside digit_sum is its own variable, so the loop that grinds it down to 0 leaves main's n at 45.
Line 2label(digit_sum(9876)) must evaluate the inner call first, because its return value is the outer call's argument.
Line 3return "sum=" + std::to_string(value); builds a std::string and copies it into the return value; the temporary lives long enough for operator<< to print it.
Important notes
The order in which arguments are evaluated is unspecified: f(i++, i) can give different results on different compilers, since C++17 only guarantees the two evaluations do not interleave.
Narrowing on the way in (2.9 to an int parameter) or on the way out (return n / 2.0; from an int function) compiles silently by default; build with -Wconversion to be told about it.
Common mistakes
Matching arguments to parameters by name: difference(right, left) puts 10 into the parameter left and yields 6, not -6, and the compiler has no reason to complain.
Leaving one branch of a non-void function without a return: the call produces an unpredictable value and the program has undefined behaviour, usually flagged only as a warning.
Printing inside the function instead of returning: a void function cannot appear in int x = show(2) + 1;, and the value is unrecoverable once it has gone to std::cout.
Try it yourself
Change, predict, then run
Write int clamp(int value, int low, int high) that returns low when value is below low, high when it is above high, and value otherwise, then print clamp(7, 1, 5), clamp(-3, 1, 5) and clamp(3, 1, 5). Delete one of the return statements and see what your compiler says about it.
Open the C++ workspaceCheck your understanding
A function is declared int mid(int a, int b) and its body is return (a + b) / 2.0;. What does std::cout << mid(3, 4); print?
- 3.5, because the expression inside return is a double
- A compile error, because a double cannot be the operand of return in an int function
- 3, because the value is converted to the declared return type before the caller sees it
- 4, because the result is rounded to the nearest int
Show answer
return initializes an object of the declared return type, so 3.5 is converted to int and the caller receives 3. 3.5 is wrong because the type of the call expression is fixed by the declaration, not by whichever expression you happen to return; 4 is wrong because the conversion truncates toward zero rather than rounding.