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.

Key Concept
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.

Key Concept
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.

Warning
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.

Best Practice
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& or std::string&
  • Modifiable lvalue: a named, non-const object, 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 const variables, literals, and temporaries
  • Choose the form by what the function does with the parameter: Type& to write back, Type for cheap reads, and const Type& for expensive reads