Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Efficient Parameter Passing by Reference
Avoid copying large objects by passing them by reference to functions.
Pass by Lvalue Reference
In previous lessons, we introduced lvalue references and lvalue references to const. In isolation, they might not have seemed particularly useful. Why create an alias when you can use the variable directly?
This lesson reveals why references are powerful. From this point forward, you'll see references used extensively.
First, let's review pass by value. When passing arguments by value, they're copied into function parameters:
#include <iostream>
void showLevel(int level)
{
std::cout << level << '\n';
} // level destroyed here
int main()
{
int playerLevel{25};
showLevel(playerLevel); // playerLevel copied into parameter level (inexpensive)
return 0;
}
When showLevel(playerLevel) is called, playerLevel's value (25) is copied into parameter level. At function's end, level is destroyed.
This means we created a copy of our argument's value, used it briefly, then destroyed it. Fortunately, fundamental types are cheap to copy, so this isn't problematic.
Some Objects Are Expensive to Copy
Most standard library types (like std::string) are class types. Class types are usually expensive to copy. We want to avoid unnecessary copies of expensive objects, especially when they're immediately destroyed.
Consider this example:
#include <iostream>
#include <string>
void displayName(std::string name)
{
std::cout << name << '\n';
} // name destroyed here
int main()
{
std::string playerName{"Sir Galahad the Brave"}; // playerName is std::string
displayName(playerName); // playerName copied into parameter name (expensive)
return 0;
}
Output:
Sir Galahad the Brave
This program works correctly but inefficiently. When displayName() is called, argument playerName is copied into parameter name. However, name is a std::string, which is expensive to copy. This expensive copy occurs every time displayName() is called!
We can do better.
Pass by Reference
One way to avoid expensive copies when calling functions is using pass by reference instead of pass by value. With pass by reference, we declare function parameters as reference types (or const reference types) rather than normal types. When the function is called, each reference parameter binds to the corresponding argument. Since references act as aliases, no copy is made.
Here's the same example using pass by reference:
#include <iostream>
#include <string>
void displayName(std::string& name) // Type changed to std::string&
{
std::cout << name << '\n';
} // name destroyed here
int main()
{
std::string playerName{"Sir Galahad the Brave"};
displayName(playerName); // playerName passed by reference into name (inexpensive)
return 0;
}
This program is identical except parameter name's type changed from std::string to std::string& (an lvalue reference). Now when displayName(playerName) is called, lvalue reference parameter name binds to argument playerName. Binding a reference is always inexpensive, and no copy of playerName is needed. Since references act as aliases, when displayName() uses name, it's actually accessing playerName (not a copy).
Pass by reference lets us pass arguments to functions without making copies each time the function is called.
This program demonstrates that value parameters are separate objects from arguments, while reference parameters are treated as the arguments themselves:
#include <iostream>
void showAddresses(int byValue, int& byReference)
{
std::cout << "Value parameter address: " << &byValue << '\n';
std::cout << "Reference parameter address: " << &byReference << '\n';
}
int main()
{
int health{100};
std::cout << "Argument address: " << &health << '\n';
showAddresses(health, health);
return 0;
}
One run produced:
Argument address: 0x7ffee8c0a8e0
Value parameter address: 0x7ffee8c0a8e4
Reference parameter address: 0x7ffee8c0a8e0
The argument has a different address from the value parameter (meaning the value parameter is a separate object). Since they have different addresses, the argument's value must be copied into the value parameter's memory.
However, the reference parameter's address matches the argument's address, meaning the reference parameter is treated as the same object as the argument.
Pass by Reference Allows Modifying Arguments
When passing objects by value, function parameters receive copies of arguments. Changes to parameter values modify the copy, not the original argument:
#include <iostream>
void heal(int health) // health is a copy of playerHealth
{
health += 25; // Modifies the copy, not playerHealth
}
int main()
{
int playerHealth{75};
std::cout << "Health: " << playerHealth << '\n';
heal(playerHealth);
std::cout << "Health: " << playerHealth << '\n'; // playerHealth unchanged
return 0;
}
Since value parameter health is a copy of playerHealth, healing only affects health. Output:
Health: 75
Health: 75
However, since references act identically to their referents, when using pass by reference, changes made to reference parameters affect the arguments:
#include <iostream>
void heal(int& health) // health bound to actual playerHealth
{
health += 25; // Modifies actual playerHealth
}
int main()
{
int playerHealth{75};
std::cout << "Health: " << playerHealth << '\n';
heal(playerHealth);
std::cout << "Health: " << playerHealth << '\n'; // playerHealth modified
return 0;
}
Output:
Health: 75
Health: 100
Initially playerHealth has value 75. When heal(playerHealth) is called, reference parameter health binds to argument playerHealth. When heal() adds to health, it's actually modifying playerHealth from 75 to 100 (not a copy). This changed value persists after heal() finishes.
Passing values by reference to non-const allows writing functions that modify passed argument values.
This capability is useful. Imagine a function determining whether a monster successfully attacked a player. If so, the monster damages the player's health. Passing the player object by reference lets the function directly modify the actual player object's health. Passing by value would only modify a copy's health, which is less useful.
Pass by Reference Only Accepts Modifiable Lvalue Arguments
Since non-const references can only bind to modifiable lvalues (essentially non-const variables), pass by reference only works with modifiable lvalue arguments. This significantly limits usefulness, as we can't pass const variables or literals:
#include <iostream>
void showHealth(int& health) // health only accepts modifiable lvalues
{
std::cout << health << '\n';
}
int main()
{
int playerHealth{100};
showHealth(playerHealth); // Valid: playerHealth is modifiable lvalue
const int maxHealth{200};
showHealth(maxHealth); // Error: maxHealth is non-modifiable lvalue
showHealth(50); // Error: 50 is rvalue
return 0;
}
Fortunately, there's an easy solution, which we'll discuss in the next lesson. We'll also examine when to use pass by value versus pass by reference.
Summary
Pass by reference: Declaring function parameters as reference types allows passing arguments without making copies—the reference parameter binds to the argument directly.
Avoiding expensive copies: Pass by reference is particularly useful for class types like std::string that are expensive to copy, avoiding performance penalties while maintaining access to the original object.
Modifying arguments: When using non-const references, functions can modify the original arguments since references act as aliases to the referents.
Limitation: Pass by non-const reference only accepts modifiable lvalue arguments, which limits its usefulness—const variables and literals cannot be passed.
Pass by reference provides an efficient alternative to pass by value for expensive-to-copy types and enables functions to modify caller arguments when needed. However, its limitation to modifiable lvalues means we often need const references for maximum flexibility.
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!