In the prior lessons, we covered the basic mechanics of l-value references and l-value references to const, but in isolation, these may not have seemed very useful. In this lesson, we’ll finally answer the question about what makes references useful, and you’ll subsequently start seeing them everywhere.
First, some context. Back in lesson 2.3 -- Introduction to function parameters and arguments we discussed pass by value
, where an argument passed to a function is copied into the function’s parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> void doSomething(int y) // y is pass by value { std::cout << y << '\n'; } int main() { int x { 2 }; doSomething(x); // value of x copied into doSomething parameter y return 0; } |
In the above program, when doSomething(x)
is called, the value of x
(2)
is copied into parameter y
.
At the end of the function, object y
is destroyed. This means we made a copy, only to use it briefly and then destroy it! Fortunately, because fundamental types (and enumerations) are cheap to copy, it’s not a problem to make copies and discard objects of these types.
However, in lesson %Failed lesson reference, id intro%, we noted that most of the types in the standard library are class types
. It turns out that class types are often expensive to copy, so we generally want to avoid making unnecessary copies (especially ones we’ll destroy almost immediately).
Pass by reference
One way to solve this problem is to use pass by reference
instead of pass by value
. When using pass by reference, we declare function parameters as references (or const references) rather than as a normal variables. Then, when the function is called, each reference parameter binds to the appropriate argument, and the function accesses the argument through the reference. This avoid making a copy of the argument.
Here’s the same example as above, using pass by reference
instead of pass by value
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> void doSomething(int& y) // y is now pass by reference { std::cout << y << '\n'; } int main() { int x { 2 }; doSomething(x); return 0; } |
Now, when doSomething(x)
is called, reference parameter y
is bound to argument x
. Since a reference to an object is treated the same as the object itself, when doSomething
uses reference y
, it’s accessing the actual object x
, not a copy!
The most notable consequence of pass by reference
is that no copy of the argument is made when the function is called. Therefore, we typically pass class types by reference to avoid paying the performance cost of making unnecessary copies. Although we haven’t introduced any class types yet, we’ll do so shortly, and now you’ll understand why we pass these by reference rather than by value!
Key insight
We typically pass class types by reference to avoid making unnecessary, expensive copies.
Best practice
Prefer passing class types (structs and classes) by (const) reference, and fundamental types (and enumerations) by value.
Author's note
Because we haven’t introduced any class types yet, in this lesson only, we’ll demonstrate pass by reference using fundamental types.
Pass by reference allows us to change the value of an argument
Because reference parameters act identically to the referenced object, this means that any changes made to the reference will affect the argument:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> void addOne(int& ref) { ++ref; } int main() { int value { 5 }; cout << "value = " << value << '\n'; addOne(value); cout << "value = " << value << '\n'; return 0; } |
This prints:
5 6
In the above program, code
initially has value 5
. However, value
is passed by reference to function addOne
, which uses the reference to change the value of value
from 5
to 6
. This updated value persists even after addOne
has terminated.
One downside of non-const reference parameters is that they can only accept arguments that are modifiable l-values (essentially, non-const variables). In practical terms, that limits their use. What happens if we want to pass a const variable, or even a literal by reference?
Pass by const reference
We can also use l-value references to const values
as function parameters. This offers the same benefit as pass by reference (avoiding making a copy) while guaranteeing that the function can not change the value being referenced. In most cases, this is what we want.
1 2 3 4 5 |
// ref is a const reference to the argument passed in, not a copy void changeN(const int& ref) { ref = 6; // not allowed: ref is const } |
References to const values are particularly useful as function parameters because of their versatility. Because an l-value reference to a const value can bind to a modifiable l-value, a non-modifiable l-value, or an r-value, a const reference parameter can bind to all of these kinds of arguments (including literals and the results of expressions):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> void printIt(const int& x) // x is a const reference { std::cout << x; } int main() { int a { 1 }; printIt(a); // ok: non-const l-value const int b { 2 }; printIt(b); // ok: const l-value printIt(3); // ok: literal r-value printIt(2+b); // ok: expression r-value return 0; } |
The above prints:
1234
Mixing pass by value and pass by reference
A function can mix pass by value and pass by reference:
1 2 3 4 5 6 7 8 9 10 11 12 |
void foo(int a, int& b) { } int main() { int x { 5 }; int y { 6 }; foo(x, y); return 0; } |
In the above example, x
is passed by value, and y
is passed by reference.
In and out parameters
Parameters passed by value or const reference 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; // uses value of ref and then 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 |
#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() { 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.
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 put 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 |
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.
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.
Best practice
Avoid out parameters
(use other methods to return multiple values), and avoid in-out parameters
when practical.
For advanced readers
Besides in
, in-out
, and out
parameters, there are two other parameter “types”: consume parameters
, and forward parameters
. Both of these are related to r-value references
and move semantics
, which we’ll talk about in a future chapter.
Return by reference
When passing by value, a copy of the argument is made into the parameter. Because this can be expensive for class types, we often choose pass by reference instead, to avoid making a copy.
Similarly, when using return by value, a copy of the return value is passed back to the caller. For class types, this can be similarly expensive. In such cases, we may (or may not) want to return by reference instead (which avoids the copy).
To return by reference, we simply define the return value of the function to be a reference type:
1 |
int& someFunction(); // this function returns a reference to an integer |
Using return by reference has one major caveat: the programmer _must_ be sure that the returned object lives beyond the scope of the function returning the reference.
Consider the following example that returns a local variable by reference:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int& someFunction() { int i { 4 }; // i is an automatic variable return i; } // i is destroyed here int main() { std::cout << someFunction(); return 0; } |
What’s the result of this program? It’s indeterminate. When someFunction
returns, a reference to local variable i
is returned. Then, because i
is an automatic variable, i
is destroyed at the end of the function. The means the returned reference is now dangling, and any use of the reference will result in undefined behavior.
Warning
Never return a local variable by reference.
Typically, references can be safely returned in one of three cases:
1) When returning a parameter passed in by reference:
1 2 3 4 |
int& max(int &x, int &y) { return (x > y) ? x : y; } |
In the above function, the caller must have passed in a valid object for x
and y
, therefore it’s safe to assume that if we return x
or y
, those objects will still exist.
2) When returning a static variable (either a local static, or a global variable). This doesn’t happen very often.
3) When returning a member of an object (including *this) from a member function. We’ll cover this case in a future chapter, when we talk about creating your own objects.
![]() |
![]() |
![]() |
I NOTICED THAT DE PROGRAM DOES NOT GIVE THE DE CORRECT ANSWER OF COS 30 AND SIN 30
.... R ...
I LEARNT A NEW LESSON AS WELL.
THANKS
I copied and pasted the following into Microsoft Visual C++ 2010 Express and ithe output is not correct. The output for 45 degress is show below my program.
Can you help me understand why?
// Passing_multiple_arguments.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#include // for sin() and cos()
void GetSinCos(double dX, double &dSin, double &dCos)
{
dSin = sin(dX);
dCos = cos(dX);
}
int _tmain(int argc, _TCHAR* argv[])
{
double dSin = 0.0;
double dCos = 0.0;
// GetSinCos will return the sin and cos in dSin and dCos
GetSinCos(45.0, dSin, dCos);
std::cout << "The sin is " << dSin << std::endl;
std::cout << "The cos is " << dCos << std::endl;
return 0;
}
/*
OUTPUT is:
The sin is 0.850904
The cos is 0.525322
*/
For some reason this query doesn't show that #include and #include were included; however, they were in the program when I ran it.
If this program is supposed to print out the sin and cos of 30 degrees it doesn't do that.
The code is wrong.
Here are figure from my Casio Calc.
For 30 degrees the sin is 0.5, cos is slightly > 0.886.
For 45 degrees the sin and cos are equal slightly > 0.707. So John M's output is wrong too.
I'm just learning C++ too, so maybe someone can give use the right code to do that.
One more thing, if you add-> using namespace std; to the top of this code. You can delete all of std:: in the code. (Smile)
"Rule: Always pass by const reference unless you need to change the value of the argument"
It's not quite true. When passing small things like built-int types, or small structures like a pair of built-in types, it's faster to copy them, than to pass reference or pointer. It allows compiler for better optimizations.