References
This lesson demystifies references, explaining how they work, their benefits, and their role in performance and data manipulation
At this point, we're hopefully familiar with calling functions, including passing arguments into our function's parameters.
However, we may have noticed, that if we modify a parameter within our function, the original variable is left unaffected:
#include <iostream>
using namespace std;
void Increment(int x){ x++; }
int main(){
int Number{1};
cout << "Number: " << Number;
Increment(Number);
cout << "\nNumber: Still " << Number;
}
Number: 1
Number: Still 1
This is often the desired behavior, but not always.
In this lesson, we'll understand why code like this doesn't work in the way we might expect. Then, we'll learn how to make it work.
Pass by Value
The behavior of our previous function is a result of how arguments get passed to parameters. When we call our functions, our arguments are copied.
So, the integer we're modifying in our function is a copy of the original variable. The variable within the calling function is not modified.
This behavior is called passing by value and, by default, it is what is used with all variable types in C++. Below, we show a user-defined type, exhibiting similar behavior:
#include <iostream>
using namespace std;
class Character {
public:
int Health{200};
};
void Combat(Character Player, Character Enemy){
Player.Health -= 50;
Enemy.Health -= 50;
}
int main(){
Character Player;
Character Enemy;
cout << "Player: " << Player.Health;
cout << "\nEnemy: " << Enemy.Health;
Combat(Player, Enemy);
cout << "\n\nPlayer: " << Player.Health;
cout << "\nEnemy: " << Enemy.Health;
}
Because the Player
and Enemy
within our Combat()
function are copies, the original Characters in our main()
function are left unscathed:
Player: 200
Enemy: 200
Player: 200
Enemy: 200
Pass by Reference
The alternative to passing arguments by value is passing them by reference.
A variable and its corresponding reference refer to the same location in our system's memory. Therefore, modifying a variable through its reference will reflect in the original variable.
To denote a reference, we append an &
to any of our other types. For example:
int&
is a reference to anint
float&
is a reference to afloat
Character&
is a reference to aCharacter
, a user-defined type
A variable can be implicitly converted to a reference to a variable of that same type so, to update our function's parameter to be passed by reference, we simply need to update its type using the &
syntax:
#include <iostream>
using namespace std;
void Increment(int& x){ x++; }
int main(){
int Number{1};
cout << "Number: " << Number;
Increment(Number);
cout << "\nNumber: Now " << Number;
}
With that, modifications to the reference within the Increment()
function are now reflected in our original variable in main()
:
Number: 1
Number: Now 2
We can do the same thing with our Combat()
function, updating our Character
types to be references, Character&
:
#include <iostream>
using namespace std;
class Character {
public:
int Health{200};
};
void Combat(Character& Player,
Character& Enemy){
Player.Health -= 50;
Enemy.Health -= 50;
}
int main(){
Character Player;
Character Enemy;
cout << "Player: " << Player.Health;
cout << "\nEnemy: " << Enemy.Health;
Combat(Player, Enemy);
cout << "\n\nPlayer: " << Player.Health;
cout << "\nEnemy: " << Enemy.Health;
}
Player: 200
Enemy: 200
Player: 150
Enemy: 150
Output Parameters
We've previously seen return
statements in action. They're the main way our functions communicate information back to their caller.
But with references, we now have another way. The caller can pass a value to our function as a reference, our function can modify it, and then the caller can inspect it after the function ends.
#include <iostream>
using namespace std;
int Attack(bool& wasFatal) {
wasFatal = true;
return 50;
}
int main() {
bool wasFatal;
int Damage{Attack(wasFatal)};
cout << "Inflicted " << Damage << " Damage";
if (wasFatal) { cout << " (Fatal)"; }
}
Inflicted 50 Damage (Fatal)
A parameter that is used for this approach is sometimes referred to as an output parameter.
Output parameters are rarely recommended. It's difficult for our function to communicate which parameters will be used in this way, and the exact nature of the modifications that could be made.
The previous function can be written in a more friendly way by returning a new type. The caller can understand what the function returns just by looking at that type, and can even unpack its values using structured binding:
#include <iostream>
using namespace std;
struct AttackResult {
int Damage;
bool wasFatal;
};
AttackResult Attack(){
return AttackResult{50, true};
}
int main(){
auto [Damage, wasFatal]{Attack()};
cout << "Inflicted " << Damage << " Damage";
if (wasFatal) { cout << " (Fatal)"; }
}
Inflicted 50 Damage (Fatal)
But output parameters have some valid use cases, and the code we're interacting with will often use them, so we should understand the pattern.
Reference Performance and const
Aside from the ability to modify parameters, passing by reference has a second, more useful property. Copying an object has a performance cost.
Therefore, if we don't need a copy within our function, using pass-by-value may have an unnecessary performance overhead.
However, the ability to modify a reference in a way that affects variables within the calling code is generally not desirable. For this reason, we can mark references as constant, using const
:
#include <iostream>
using namespace std;
class Character {
public:
int Health{200};
};
void SomeFunction(const Character& Player){
// We can do this with a const reference:
cout << Player.Health;
// But we can't do this:
Player.Health -= 50;
}
int main(){
Character Player;
SomeFunction(Player);
}
error: 'Health' cannot be modified because it is
being accessed through a const object
We can use the keyword const
in many situations in C++, all relating to the idea of defining data that cannot be modified. We'll see more examples later in the course.
With a const
reference, we can read the data, but not change it. This includes indirect modification by, for example, passing the reference off to another function that hasn't marked the parameter as const
.
Constant references get us the performance benefits of passing by reference, whilst also letting the caller know we're not going to modify their argument.
Creating References
The use of references goes beyond just passing data to functions. We can use references at any time.
Below, we simply create a reference like any other variable. Our identifier Ref
is bound to the MyNumber
variable. As such, modifications to Ref
affect MyNumber
:
#include <iostream>
using namespace std;
int main(){
int MyNumber{5};
int& Ref{MyNumber};
Ref++;
cout << "Value: " << MyNumber;
}
Value: 6
Reference Restrictions
References are closely related to the concept of memory addresses, which we'll cover in more detail throughout this chapter.
Working with memory addresses is a common source of bugs. References implement two restrictions that eliminate a large source of these defects:
References Must be Initialized
When we create a reference, we must also initialize it to point to the desired memory address - usually by assigning it to a variable:
int main(){
int MyNumber{5};
int& Ref{MyNumber};
}
If we try to declare a reference without providing it with an initial value, we will get a compilation error:
int main(){
int& Ref;
}
error: 'Ref': references must be initialized
References Cannot be Reassigned
The second restriction is that a reference cannot be updated to point to a different memory address. Once a reference is created, it will always point to the memory address it was initialized with.
In the next lesson, we'll introduce pointers. These use similar concepts to references, but they remove these two restrictions. This makes pointers more dangerous to work with, but also more powerful.
References as Class Members
Finally, let's see an example of a reference within a user-defined type. Below, our Character
objects have a member mSword
, which is a reference to a Weapon
:
class Weapon {
public:
string Name;
};
class Character {
public:
Character(Weapon& Sword): mSword(Sword){}
Weapon GetWeapon(){ return mSword; }
private:
Weapon& mSword;
};
When our classes contain references, the references must be initialized within a member initializer list.
Attempting to set a reference within a constructor body is too late to satisfy the "references must be initialized" restriction. We covered the reasons for this in more detail in our earlier lesson:
Member Initializer Lists
This lesson introduces Member Initializer Lists, focusing on their advantages for performance and readability, and how to use them effectively
References within an object work the same way as they do anywhere else. Below, we modify PlayerWeapon
and, since our Character
is holding that object by reference, we see the changes reflected in the Player.GetWeapon()
return value:
#include <iostream>
using namespace std;
class Weapon {
public:
string Name;
};
class Character {
public:
Character(Weapon& Sword): mSword(Sword){}
Weapon GetWeapon(){ return mSword; }
private:
Weapon& mSword;
};
int main(){
Weapon PlayerWeapon{"Wooden Sword"};
Character Player{PlayerWeapon};
cout << Player.GetWeapon().Name << '\n';
PlayerWeapon.Name = "Steel Sword";
cout << Player.GetWeapon().Name;
}
Wooden Sword
Steel Sword
Test your Knowledge
References
Consider the following variable:
int SomeVariable{42};
What is the correct syntax for declaring a reference to SomeVariable
?
What is the result of modifying a variable through its reference?
What is the primary purpose of using references in functions?
What happens if you try to declare a reference without initializing it?
Summary
This lesson provided an introduction to the concept of references. We covered:
- Understanding the difference between passing by value and passing by reference.
- Learning how to use the
&
symbol to create references. - Exploring the use of references in functions, including output parameters and constant references.
- Discussing the restrictions and rules governing the use of references.
- Examining the role of references in user-defined types, such as class members.
Pointers
This lesson provides a thorough introduction to pointers in C++, covering their definition, usage, and the distinction between pointers and references