Efficient Parameter Passing by Reference
Avoid copying large objects by passing them by reference to functions.
What Is Pass by Lvalue Reference?
Declare a function parameter as Type& and the parameter stops being a variable of its own. For the duration of the call it is a second name for the object the caller supplied, bound at the call site and released when the function returns. That is pass by lvalue reference, and it is the point at which references stop being a curiosity and start being something you reach for daily.
The lessons on lvalue references and lvalue references to const built the machinery without giving you a reason to want it, because an alias to a variable you can already name looks like a solution without a problem. Function parameters are where the problem lives. The caller's object and the function's parameter are ordinarily two different objects, and everything awkward about pass by value follows from that gap.
So this lesson turns on a single question, asked of every parameter you write: is this parameter a new object, or is it the caller's object under another name? Answer it and the cost of the call, and whether the function can write back to the caller, both fall out automatically.
The Parameter Is Not a New Object
The cleanest way to see the difference is to hand the same variable to both kinds of parameter in one call, then write through the reference and see who notices:
#include <iostream>
void compareParameters(int snapshot, int& live)
{
live = 96;
std::cout << "snapshot: " << snapshot << '\n';
std::cout << "live: " << live << '\n';
}
int main()
{
int seatsSold{40};
compareParameters(seatsSold, seatsSold); // one object, handed over twice
std::cout << "seatsSold: " << seatsSold << '\n';
return 0;
}
snapshot: 40
live: 96
seatsSold: 96
Both arguments were the same variable, yet after the assignment the two parameters disagree. snapshot was filled in by copying seatsSold at the moment of the call, and it has been a separate int ever since; nothing done to live can reach it. live never had its own storage. Writing 96 through it wrote 96 into seatsSold, which is why the last line agrees with live rather than with snapshot.
Take the address of each of the three names and the same story shows up in the numbers: snapshot reports an address of its own, while live and seatsSold report the same one, because there is only one object there to have an address. The address-of operator arrives properly in a later lesson in this chapter, but the conclusion does not need it.
A value parameter is an object that did not exist before the call and will not exist after it. A reference parameter is not an object at all; it is a name the function uses for something the caller owns.
Two consequences follow from that, and they are worth separating, because a given function usually wants one of them and merely tolerates the other.
Consequence One: Nothing Is Copied
Copying an int costs a register move, so nobody worries about it. Copying a std::string is a different matter. std::string is a class type, and the characters it manages usually live in a separately allocated block, so duplicating one means allocating a fresh block, copying every character into it, and freeing it again when the copy dies. A function that only wants to read the string has paid for all of that and thrown the result away.
Binding a reference does none of it. The cost of binding is the same whether the object behind it holds twenty characters or twenty million:
#include <iostream>
#include <string>
void postByValue(std::string marquee) // a second std::string, built by copying
{
std::cout << "by value: " << marquee << '\n';
}
void postByReference(std::string& marquee) // another name for the caller's string
{
std::cout << "by reference: " << marquee << '\n';
}
int main()
{
std::string headline{"Winter on the Danube"};
std::cout << "characters in headline: " << headline.size() << '\n';
postByValue(headline); // duplicates every one of them
postByReference(headline); // duplicates none of them
return 0;
}
characters in headline: 20
by value: Winter on the Danube
by reference: Winter on the Danube
The two functions print identical text, which is exactly the point: the difference is invisible in the output and entirely in the work done to produce it. One & in the parameter list removed twenty character copies and an allocation from the call, and it would remove twenty million from a call with a longer headline.
Note that this argument is about class types, not about references in general. For an int, a double, or another type that fits in a register, a copy is already as cheap as anything can be, and routing access through a reference can even cost slightly more. Pass by reference is a cure for expensive copies, not a universal speed-up.
Consequence Two: The Function Can Write Back
The other half of the bargain is that a non-const reference parameter is a licence to modify the caller's object. Nothing special is needed to use it; an ordinary assignment inside the function lands on the caller's variable:
#include <iostream>
void bookSeats(int& seatsSold, int quantity)
{
seatsSold += quantity;
}
int main()
{
int seatsSold{40};
std::cout << "before: " << seatsSold << '\n';
bookSeats(seatsSold, 12);
std::cout << "after: " << seatsSold << '\n';
return 0;
}
before: 40
after: 52
bookSeats returns nothing, and yet the caller's seatsSold is twelve higher afterwards. The change outlives the call because it was never made to anything temporary: seatsSold inside the function and seatsSold inside main are the same object throughout.
Compare that with the by-value version. Change the parameter to int seatsSold and the function still compiles, still runs, still adds twelve, and accomplishes nothing at all, because the twelve lands on a copy that is destroyed at the closing brace. This is the failure compareParameters demonstrated in the first section, met in the wild.
The two parameters in bookSeats illustrate the choice being made per parameter rather than per function. seatsSold is a reference because the whole purpose of the call is to change it. quantity is a plain int because the function only reads it and an int is free to copy.
A parameter of type
Type& is how a function reports a result back through its parameter list instead of through its return value. That is the standard way to hand back more than one piece of information from a single call.
Where this matters most is with objects too big or too structured to return conveniently. A function deciding whether a monster's attack lands can take the player by reference and subtract the damage from the real player's health; taking the player by value would subtract it from a copy that is thrown away on return.
The Price: Only Modifiable Lvalues May Be Passed
A non-const lvalue reference can bind to a modifiable lvalue and to nothing else, and that restriction transfers straight to the parameter. Anything the caller cannot legally be given permission to modify is rejected at the call. This program does not compile:
#include <iostream>
void bookSeats(int& seatsSold)
{
++seatsSold;
}
int main()
{
int matinee{18};
bookSeats(matinee); // fine: matinee is a modifiable lvalue
const int houseCapacity{320};
bookSeats(houseCapacity); // will not compile
bookSeats(7); // will not compile
std::cout << matinee << '\n';
return 0;
}
s.cpp: In function 'int main()':
s.cpp:14:15: error: binding reference of type 'int&' to 'const int' discards qualifiers
14 | bookSeats(houseCapacity); // will not compile
| ^~~~~~~~~~~~~
s.cpp:3:21: note: initializing argument 1 of 'void bookSeats(int&)'
3 | void bookSeats(int& seatsSold)
| ~~~~~^~~~~~~~~
s.cpp:16:15: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
16 | bookSeats(7); // will not compile
| ^
s.cpp:3:21: note: initializing argument 1 of 'void bookSeats(int&)'
3 | void bookSeats(int& seatsSold)
| ~~~~~^~~~~~~~~
The two rejections have the same root and different wording. houseCapacity is an lvalue, so it has an address and a name, but it is const, and binding it to int& would hand the function a route to modify a constant. The literal 7 is an rvalue: there is no object to bind to in the first place.
The rule is doing its job here, since bookSeats really does write to its parameter. The trouble is that the signature imposes it on every caller, including the ones that only wanted the function to read. A read-only function with an int& or std::string& parameter rejects constants, literals, and temporaries for no benefit whatsoever.
Reaching for pass by reference purely to skip a copy, and forgetting the
const, narrows the set of arguments the function accepts and advertises a modification the function never performs. The next lesson fixes both problems with a single keyword.
Choosing a Parameter Type Today
With only two of the three parameter forms taught so far, the decision is already worth writing down:
| The function needs to | Parameter form | Why |
|---|---|---|
| change the caller's object | Type& |
the only form of the three that reaches back to the caller |
read a cheap type such as int or double |
Type |
a copy is already free, and the copy cannot be disturbed by anything the caller does |
read an expensive type such as std::string |
const Type& |
binds without copying, and accepts constants, literals, and temporaries as well |
The third row is next lesson's material, but it is listed here because it is the row most parameters in real code fall into, and because leaving it out would make non-const references look like the answer to a question they only half answer.
Let the direction of the data pick the parameter form. A function that has to hand something back through its parameter list needs
Type&; one that only reads should not be using a non-const reference at all, whatever the size of the argument.
Looking Forward
The next lesson adds const to the reference parameter, which keeps the no-copy behaviour while dropping the modifiable-lvalue restriction, and settles when each of the three forms is the right choice. Later in this chapter, pass by address covers the fourth option: handing the function a pointer, which is what you need when the argument may legitimately be absent.
Key Terminology
- Pass by value: the parameter is a new object initialised by copying the argument
- Pass by lvalue reference: the parameter is a reference bound to the caller's argument, so no copy is made
- Reference parameter: a parameter declared with a reference type, such as
int&orstd::string& - Modifiable lvalue: a named, non-
constobject, the only kind of argument a non-const reference parameter accepts
Summary
- A reference parameter is bound to the caller's argument rather than initialised from it, so the parameter and the argument are one object with one address
- A value parameter is a separate object that stops existing when the function returns, so writes to it never reach the caller
- Binding a reference costs the same regardless of the size of the object, which is what makes it the cure for copying class types such as
std::string - Fundamental types are already cheap to copy, so pass by reference is not automatically faster than pass by value
- A non-const reference parameter lets a function report results back through its parameter list, and those changes persist after the call
- A non-const lvalue reference parameter accepts only modifiable lvalues, rejecting
constvariables, literals, and temporaries - Choose the form by what the function does with the parameter:
Type&to write back,Typefor cheap reads, andconst Type&for expensive reads
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Efficient Parameter Passing by Reference - Quiz
Test your understanding of the lesson.
Practice Exercises
Pass by Lvalue Reference
Learn to pass expensive-to-copy objects by reference to avoid unnecessary copies. Understand how to modify arguments through non-const references.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!