Hello! Welcome to the third lesson in our course on C++ internals.
In our last lesson, we delved into the Itanium C++ ABI and deconstructed how name mangling works. You saw how the compiler encodes function signatures into unique symbols for the linker, enabling features like namespaces and setting the stage for function overloading. This process is one of the key low-level distinctions between C and C++.
Today, we'll explore another feature unique to C++: references. From your C background, you are an expert in using pointers for indirect access to memory. This lesson will introduce references as a C++ alternative. We will analyze their behavior at the language level, but most importantly, we will go "under the hood" to see how they are typically implemented at the assembly level, directly comparing them to the pointers you already know so well.
What is a C++ Reference?
At a high level, a reference is an alias or an alternative name for an existing variable. Once you initialize a reference to refer to a variable, you can use the reference's name and the original variable's name interchangeably.
To see the basic syntax and behavior, let's start with a short video.
The video 'what even is a "reference"?' from the Low Level channel provides a quick and clear introduction to the concept.
Watch from 00:42 to 02:05. Focus on how a reference is declared (&) and initialized, and notice how modifying the reference also modifies the original variable.
As the video shows, the syntax is simple:
int i = 10;
int& r = i; // r is now a reference to i
r = 20; // This changes the value of i to 20
References vs. Pointers: The Language-Level Rules
Your first thought might be, "This seems a lot like a pointer." You're right to think that, but at the C++ language level, there are strict rules that differentiate references from pointers. These rules are designed to make references safer and often easier to use.
Let's watch the next part of the same video, which enumerates these differences.
This clip details the four key distinctions between references and pointers.
Watch from 02:05 to 02:53. Pay attention to each of the four points the video makes.
To summarize and expand on these crucial differences:
- Must Be Initialized: A reference must be initialized to refer to an existing object when it is declared. You cannot have an uninitialized reference.
int& r; // Compile error! int* p; // Perfectly legal. p holds a garbage address. - Cannot Be Null: Because a reference must be initialized with a valid object, there is no concept of a "null reference". This eliminates an entire class of bugs related to null pointer dereferencing.
- Cannot Be Reassigned: Once a reference is initialized to refer to a variable, it cannot be "reseated" or changed to refer to a different variable. It is an alias for that original variable for its entire lifetime.
int x = 5; int y = 10; int& r = x; // r refers to x r = y; // This does NOT make r refer to y. // It assigns the value of y (10) to the variable r refers to (x). // So, x now becomes 10. - No "Reference Arithmetic": You cannot perform pointer arithmetic on references. They don't have the same address-manipulation capabilities as pointers.
These rules are enforced by the compiler. They allow you to pass objects to functions for modification with the guarantee that you're dealing with a valid object, not a potential nullptr.
Under the Hood: The Assembly-Level Implementation
Now for the central question, and the one most relevant to your goals: if references and pointers are so different at the language level, how are they implemented at the machine level?
The answer is often surprising: a reference is typically implemented by the compiler as a pointer.
Let's see the evidence. The final part of the "Low Level" video uses objdump to compare the assembly generated for code using a pointer versus code using a reference.
This segment gets to the core of our lesson, showing that despite the high-level differences, the compiler generates nearly identical machine code for references and pointers.
Watch from 03:23 to 05:32. The key insight here is observing how the lea (Load Effective Address) instruction is used for both creating a pointer and creating a reference, indicating they both just store the variable's address.
The video's conclusion is spot on: a reference is essentially "syntactic sugar" for a pointer that is constant (cannot be reseated) and is always dereferenced automatically. The compiler enforces the high-level rules, but when it generates machine code, it just uses an address.
To solidify this, let's look at another excellent analysis from a Stack Overflow discussion.
C++ assembler output - how are references implemented
This Stack Overflow answer provides two powerful demonstrations. The first shows identical assembly for two swap functions (one using pointers, one using references). The second provides a line-by-line GDB disassembly to trace the memory usage of a reference on the stack.
First, read the answer from user Olaf Dietsche (it's the one that starts with 'A reference is nothing more than a pointer...'). Note how the assembly for _Z5swap1RiS_ (reference version) and _Z5swap2PiS_ (pointer version) is identical. Then, read the longer, more detailed answer below it from user Prasad Rane. Follow his GDB analysis to see how the reference refToI is allocated its own space on the stack to store the address of i, just like a pointer.

Test your understanding!
Consider the following C++ function and its corresponding x86-64 assembly generated by g++ -O0:
C++ Code:
void increment(int& val) {
val = val + 1;
}
Assembly:
increment(int&):
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi ; Move the address of 'val' (passed in rdi) to the stack
mov rax, QWORD PTR [rbp-8] ; Load the address of 'val' into rax
mov eax, DWORD PTR [rax] ; Dereference rax to get the value of 'val' into eax
add eax, 1 ; Increment the value
mov rdx, QWORD PTR [rbp-8] ; Load the address of 'val' into rdx again
mov DWORD PTR [rdx], eax ; Store the new value back at that address
nop
pop rbp
ret
How does this assembly code demonstrate that the reference val is being treated as a pointer "under the hood"?
Show answer
The assembly clearly shows pointer-like behavior.
- The argument passed in the
rdiregister is an address. We know this because the code immediately dereferences it (mov eax, DWORD PTR [rax]) to get the integer value. - The code explicitly loads the address from the stack into a register (
rax,rdx) and then uses that address to read ([rax]) and write ([rdx]) the value.
This is exactly how you would expect code operating on a pointer (int*) to behave: load the address, then dereference it to access the data. The C++ compiler has translated the clean reference syntax into explicit pointer operations in the machine code.
Practical Consequences: The const Reference vs. Value Debate
Knowing that a reference is implemented as a pointer has very practical performance implications. A common C++ idiom is to pass large objects by const reference to avoid expensive copies. But is this always the best approach?
The answer depends on the size of the object.
Should I pass by const reference or by value?
The Cherno provides an excellent discussion on the trade-offs between passing by value and passing by const reference, grounded in the underlying implementation.
First, watch from 00:31 to 02:50. He explains why passing a small type like uint32_t (4 bytes) by const reference can be slower on a 64-bit system than passing by value. Then, watch from 04:34 to 07:41 where he explains why you must use a const reference for complex, non-trivially-copyable types to avoid deep, expensive copy operations.
This leads to a crucial rule of thumb in C++:
- For small, trivially copyable types (like
int,char,double,float): Pass by value. It's often more efficient because the value can be passed directly in a CPU register, avoiding the memory indirection of a reference (pointer). - For large or complex types (like strings, vectors, or custom classes that manage resources): Pass by
constreference (if you don't need to modify it) or by reference (if you do). This avoids making a potentially slow and memory-intensive copy of the entire object.
Conclusion
In this lesson, we demystified C++ references by comparing them directly to your existing knowledge of C pointers. You now understand not only the language-level safety guarantees they provide but also how the compiler translates them into familiar pointer operations at the assembly level.
Key Takeaways:
- A reference is an alias for a variable, providing safer, cleaner syntax for indirect access compared to pointers.
- At the language level, references cannot be null, cannot be reassigned, and must be initialized. These rules are enforced by the compiler.
- At the machine level, a reference is typically implemented as a pointer. The compiler generates code that loads the address and automatically dereferences it.
- The choice to pass an argument by value or by reference is a performance decision:
- Pass-by-value is usually better for small, cheap-to-copy types.
- Pass-by-reference is essential for large or complex types to avoid expensive copies.
In our next lesson, we'll build on this foundation. We will revisit the topic of function overloading and analyze exactly how the compiler uses the types of arguments, including references, to resolve which function to call, tying back to the name mangling concepts we learned earlier.