References

This lesson demystifies references, explaining how they work, their benefits, and their role in performance and data manipulation

Ryan McCombe
Updated

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 an int
  • float& is a reference to a float
  • Character& is a reference to a Character, 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.
Next Lesson
Lesson 31 of 60

Pointers

This lesson provides a thorough introduction to pointers in C++, covering their definition, usage, and the distinction between pointers and references

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Swapping Values Using References
How can I use references to swap the values of two variables without using a temporary variable?
Creating an Array of References
Is it possible to create an array of references in C++?
References and Runtime Polymorphism
Can I use references with polymorphic classes to achieve runtime polymorphism?
Implementing Observer Pattern with References
How can I use references to implement a simple observer pattern in C++?
Reference vs Pointer to Const
What's the difference between a reference and a pointer to const in terms of function parameters?
Dangers of Returning References to Local Objects
What are the implications of returning a reference from a function that creates a local object?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant