Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

C++ References: Efficient Parameters and Aliasing

Recommended resources

Hello! Welcome to the fifth lesson in your "C++ for ESP32" course.

Introduction

In our last lesson, we focused on encapsulation, using access specifiers (public, private) to protect a class's internal data and create a robust public interface. We refactored a Led class to prevent direct manipulation of its member variables, ensuring its state remained consistent.

This raised a practical question: how do we pass objects like our Led to other functions? If we pass it by value, the compiler creates a complete copy, which is inefficient and can have unintended side effects (like calling constructors and destructors for the temporary copy). We could use pointers, as you might be familiar with from C, but C++ offers a more elegant and often safer alternative.

Today's lesson directly addresses this. The learning outcome is to use references in C++ for efficient parameter passing and aliasing. We'll explore what references are, how they differ from pointers, and why they are a cornerstone of modern C++ programming, especially in performance-critical embedded systems.


1. What is a Reference?

At its core, a C++ reference is an alias for an existing variable. It's another name for the same location in memory. Once you create a reference, you can use it just as you would use the original variable.

Let's start with a video that introduces this core concept and the basic syntax.

REFERENCES in C++

This video from The Cherno provides a clear and concise introduction to references, explaining them as aliases and showing the fundamental syntax.

Please watch from 00:57 to 03:54. Focus on how a reference is declared and how modifying the reference affects the original variable.

As the video explained, you declare a reference using the ampersand (&) in the type declaration:

#include <iostream>

int main() {
    int original_value = 10;

    // ref_value is now an alias for original_value.
    // It's not a new variable holding a copy; it IS original_value.
    int& ref_value = original_value;

    std::cout << "Original: " << original_value << std::endl; // Prints 10
    std::cout << "Reference: " << ref_value << std::endl;    // Prints 10

    // Modifying the reference...
    ref_value = 20;

    // ...also modifies the original.
    std::cout << "Original after change: " << original_value << std::endl; // Prints 20
    std::cout << "Reference after change: " << ref_value << std::endl;    // Prints 20
}

There are two critical rules for references:

  1. They must be initialized when declared. A reference cannot exist without being an alias for something. int& my_ref; is a compile error.
  2. They cannot be "reseated" to refer to another variable. Once a reference is an alias for variable_A, it will always be an alias for variable_A.

2. References vs. Pointers

Given your background in C, references might seem very similar to pointers. They both allow indirect access to a variable. However, they have crucial differences in syntax and behavior.

The following video does an excellent job of comparing them and explaining how references work "under the hood," which is particularly insightful for embedded development.

what even is a "reference"?

This video from Low Level provides a fantastic breakdown of the differences between references and pointers, and then reveals that references are essentially a compiler-enforced, safer version of pointers.

Please watch from 01:51 to 05:32. The first part (until 03:23) lists the key differences. The second part shows the assembly code to demonstrate that they are implemented similarly at a low level.

Let's summarize the key distinctions:

Feature Pointers (int* p) References (int& r)
Initialization Can be uninitialized or nullptr. Must be initialized to an existing variable.
Reassignment Can be changed to point to another variable. Cannot be changed to refer to another variable.
Nullability Can be nullptr. Cannot be null. This eliminates the need for null checks.
Syntax Requires explicit dereferencing (*p) and address-of (&var). Uses the same syntax as the original variable.
Arithmetic Pointer arithmetic is allowed (p++). Not allowed.

The most interesting insight from the video is that at the machine code level, a reference is typically implemented as a pointer. The C++ compiler simply adds a layer of safety and convenience, enforcing the rules (no nulls, no reassignment) at compile time. You can think of a reference as a "spicy pointer" or, more formally, as syntactic sugar over a non-nullable, non-reassignable pointer.


3. The Primary Use Case: Efficient Parameter Passing

The most common and important use of references is for passing arguments to functions. This allows a function to operate on the original variable without the overhead of making a copy.

Let's see a demonstration of the three ways to pass a variable to a function: by value, by pointer, and by reference.

REFERENCES in C++

We'll return to The Cherno's video, which now perfectly illustrates why references are so useful for function parameters. It compares the three methods side-by-side.

Watch from 03:54 to 07:30. Pay close attention to the evolution of the increment function, from pass-by-value, to pass-by-pointer, and finally to the much cleaner pass-by-reference.

As the video demonstrated:

  1. Pass-by-Value (void func(int val)):

    • Creates a copy of the argument.
    • Inefficient for large objects (structs, classes).
    • The function cannot modify the original variable.
  2. Pass-by-Pointer (void func(int* ptr)):

    • Passes the memory address. Very efficient.
    • The function can modify the original variable via dereferencing (*ptr).
    • Syntax is less clean (->, *).
    • The caller must use the address-of operator (&var).
    • The pointer could be nullptr, requiring a check.
  3. Pass-by-Reference (void func(int& ref)):

    • Passes an alias to the original variable. Very efficient.
    • The function can modify the original variable.
    • Syntax is clean, just like using the variable directly.
    • The caller passes the variable normally (var), not its address.
    • Guaranteed to refer to a valid object (not null).

For these reasons, pass-by-reference is the preferred method in C++ for passing objects that you intend to modify or that are expensive to copy.


4. An ESP32 Example: Toggling an LED

Let's apply this to our Led class from the previous lesson. Imagine we want to write a helper function that toggles an LED's state.

// Assume our encapsulated Led class from the last lesson exists here
class Led { /* ... as before ... */ };

// Option 1: Pass-by-Value (BAD)
void toggle_led_by_value(Led led) {
    // 'led' is a COPY. The constructor for the copy is called.
    if (led.get_status()) {
        led.turn_off();
    } else {
        led.turn_on();
    }
    // When this function ends, the destructor for the 'led' copy is called.
    // This is inefficient and might even de-configure the original pin!
}

// Option 2: Pass-by-Pointer (Okay, but not ideal)
void toggle_led_by_pointer(Led* led_ptr) {
    if (led_ptr == nullptr) return; // Must check for null

    if (led_ptr->get_status()) { // Note the -> operator
        led_ptr->turn_off();
    } else {
        led_ptr->turn_on();
    }
}

// Option 3: Pass-by-Reference (BEST)
void toggle_led_by_reference(Led& led_ref) {
    // No need to check for null.
    // We are working directly on the original Led object.
    if (led_ref.get_status()) { // Note the clean . operator
        led_ref.turn_off();
    } else {
        led_ref.turn_on();
    }
}

int main() {
    Led my_led(13); // GPIO pin 13

    // Calling the functions:
    // toggle_led_by_value(my_led); // Avoid this!

    toggle_led_by_pointer(&my_led); // Works, but syntax is less clean

    toggle_led_by_reference(my_led); // Clean, efficient, and safe. The C++ way.
}

For embedded systems like the ESP32, avoiding the unnecessary copy in toggle_led_by_value is critical. The copy consumes stack memory and CPU cycles for both the constructor and destructor calls. Pass-by-reference gives us the efficiency of pointers with greater safety and readability.


Conclusion

You've now learned about one of the most powerful and idiomatic features of C++. References provide a mechanism for creating aliases, which is primarily used to pass objects to functions efficiently and cleanly.

Key Takeaways:

  • A reference is an alias for an existing variable. It is not a new variable and does not occupy its own memory in the same way.
  • References must be initialized upon creation and cannot be reseated to refer to a different variable.
  • Compared to pointers, references offer safer syntax (no explicit dereferencing) and the guarantee that they are not null.
  • Pass-by-reference is the preferred C++ method for passing objects to functions when you want to avoid expensive copies or need to modify the original object.

In our next lesson, we will dive deeper into memory management by exploring the difference between stack and heap memory allocation. Understanding this is crucial, as it dictates where our variables and objects live, how long they persist, and how we manage them throughout our program's lifecycle.