Hello! Welcome back to your journey in building a high-performance telemetry engine.
In our last lesson, you learned to direct your program's logic using conditionals (if, else) and loops (for, while). You also took a peek under the hood to see how these constructs translate into machine instructionsโa crucial perspective for performance-oriented programming.
Now that you can store data and control program flow, it's time to organize your code into logical, reusable units. This lesson is about functions. We will cover how to define and call them, but more importantly, we will dissect the three fundamental ways C++ passes data to functions:
- Pass-by-value
- Pass-by-pointer
- Pass-by-reference
Understanding the difference isn't just academic; it's at the heart of your project's "zero-overhead" design goal. When your telemetry system is capturing thousands of events per second, the cost of passing event data from one function to another becomes a major performance factor. This lesson will give you the tools to make those operations efficient.
Functions: The Building Blocks of Programs
A function is a named block of code that performs a specific task. Functions allow you to break down a complex problem into smaller, manageable pieces.
A function definition in C++ has four main parts:
- Return type: The type of the value the function sends back (e.g.,
int, orvoidif it returns nothing). - Function name: An identifier for the function.
- Parameters: A list of variables that the function accepts as input.
- Function body: The block of code, enclosed in
{}, that gets executed.
Here's a simple example:
// A function that takes two integers and returns their sum.
int add(int a, int b) { // Return type: int, Name: add, Parameters: a, b
// Function body starts here
int result = a + b;
return result; // Returns the value of result
// Function body ends here
}
int main() {
int sum = add(5, 3); // Calling the function 'add'
// 'sum' is now 8
return 0;
}
The way we define the parameters int a and int b determines how data is passed into the function. Let's explore the three mechanisms.

Part 1: Pass-by-Value
Pass-by-value is the default mechanism in C++. When you pass an argument by value, the function receives a copy of the data. Any modifications made to the parameter inside the function do not affect the original variable in the code that called it.
To understand this visually, let's watch a short segment of the following video.
C++ Tutorial 13: Passing by Reference vs Pointer
The video 'C++ Tutorial 13: Passing by Reference vs Pointer' by Creel provides an excellent visual walkthrough of how pass-by-value works. Pay attention to how it depicts the variables on the stack.
Watch from 0:48 to 5:49. The video explains that a copy of the variable is made, so the function operates on a separate piece of memory, leaving the original untouched.
As the video demonstrated, the function add25 received a copy of the variable a. The original a in main and the parameter a in add25 are two distinct variables at different memory locations.
For a more detailed explanation with code you can run, please read the following sections of the provided article.
Pass by Value and Pass by Reference
The article 'Pass by Value and Pass by Reference' from UCL clearly explains the concepts of memory addresses and provides a code example that prints out the addresses of variables to prove that pass-by-value creates copies.
Read the sections 'Pass by Value and Pass by Reference' (the introduction) and 'Pass by Value'. Focus on how the & operator is used to get a variable's memory address and how the output of the code example shows different addresses for the variables in main versus the parameters in the add function.
Pros & Cons of Pass-by-Value:
- Pro: It's safe. The function cannot unintentionally modify the caller's data. This makes the code easier to reason about.
- Con: It can be inefficient. For large data structures (like a detailed event from your profiler), creating a copy can consume significant time and memory. This is the "observer effect" we want to avoid.
Part 2: Indirection via Pointers and References
What if we want a function to modify the original data, or what if we want to avoid the cost of copying? The solution is indirection: instead of passing the data itself, we pass its memory address.

C++ provides two ways to work with memory addresses: pointers and references.
Pass-by-Pointer
A pointer is a variable that stores a memory address. By passing a pointer to a function, you give that function the address of the original data, allowing it to "reach back" and modify it.
The syntax involves three key operators:
*(in declaration): Declares a pointer variable (e.g.,int* p;).&(address-of): Gets the memory address of a variable (e.g.,p = &my_var;).*(dereference): Accesses the value at the address stored in a pointer (e.g.,*p = 10;).
Let's see this in action.
C++ Tutorial 13: Passing by Reference vs Pointer
The same video from Creel now explains pass-by-pointer. This segment clearly shows how passing an address allows the function to modify the original variable.
Watch from 5:49 to 9:57. Notice the syntax change in the function call (&a) and within the function body (*a).
Pros & Cons of Pass-by-Pointer:
- Pro: Efficient for large objects, as only a small address is copied. It allows for intentional modification of the caller's data.
- Con: The syntax is more complex (
*,&). Pointers can benullptr(point to nothing), which requires careful checks to avoid crashes.
Pass-by-Reference (The C++ Way)
C++ introduced references to provide a safer and cleaner syntax for indirection. A reference is an aliasโanother name for an existing variable.
When you pass by reference, the syntax becomes much simpler:
- In the function signature, you use
&to declare a reference parameter (e.g.,void func(int& ref);). - In the function body and the function call, you use the variable names directly, without any special operators.
C++ Tutorial 13: Passing by Reference vs Pointer
This final segment from Creel's video introduces pass-by-reference as a more convenient alternative to pointers for the same goal.
Watch from 9:57 to 14:13. Observe how the syntax is cleaner than with pointers, but the end result (modifying the original variable) is the same.
Part 3: References vs. Pointers - The Low-Level View
So, if references and pointers both achieve indirection, what's the real difference? This is a critical question for a systems programmer.
A reference is a compile-time concept that provides guarantees. A pointer is a variable that holds an address. The key insight is that under the hood, the compiler often implements a reference as a pointer. The difference lies in the rules the compiler enforces on you.
This next video is perfect for your low-level interests, as it proves this by looking at the generated assembly code.
The video 'what even is a "reference"?' from the Low Level channel is a fantastic deep dive. It demystifies references by showing they are essentially compiler-managed pointers.
Watch from 0:42 to 5:24. Pay close attention to: The definition of a reference as an alias. The key differences listed (nullability, reassignment). The disassembly section where the author shows that the assembly code for using a reference and a pointer can be identical.
Here's a summary of the fundamental differences:
| Feature | Pointers | References |
|---|---|---|
| Nullability | Can be nullptr, meaning they point to nothing. |
Must be initialized to refer to a valid object. Cannot be null. |
| Reassignment | Can be changed to point to a different object. | Cannot be "reseated." Always refers to the same object it was initialized with. |
| Syntax | Requires explicit dereferencing (*) and address-of (&). |
Used directly like the original variable. Syntax is transparent. |
| Arithmetic | Supports pointer arithmetic (e.g., ptr++). |
Does not support arithmetic. |
Part 4: Practical Guidelines: Which One Should I Use?
With three options, how do you choose? Here are the standard C++ guidelines, which prioritize both safety and performance.
Pass by Value and Pass by Reference
This article from UCL provides excellent, practical advice on when to use each method and introduces the most important C++ idiom for passing parameters: the const reference.
Read the sections 'Pass by Reference', 'Using const in Pass By Reference', and 'Which should I use?'. This will solidify your understanding and give you a clear decision-making framework.
Here is a summary of the decision-making process:
- Does the function need to modify the original object?
- No (read-only access):
- Is the object small and cheap to copy (like
int,double,float)? -> Pass-by-value. - Is the object large or expensive to copy (like a
structorclass)? -> Pass byconstreference.// Efficient and safe: No copy is made, and the function can't change my_event. void log_event(const TelemetryEvent& my_event);
- Is the object small and cheap to copy (like
- Yes (modification is intended):
- Does the object have to exist (is
nullnot a valid state)? -> Pass-by-reference.// The function expects a valid event to modify. void add_timestamp(TelemetryEvent& my_event); - Could the object be optional (is
nulla valid state, meaning "no object")? -> Pass-by-pointer.// The function might or might not be given an event to process. void process_optional_event(TelemetryEvent* my_event);
- Does the object have to exist (is
- No (read-only access):
Passing by const reference is the default choice for passing large, read-only objects in modern C++. It gives you the performance of a pointer with the safety of pass-by-value.
Test your understanding!
Imagine you have a struct for a telemetry event:
struct TelemetryEvent {
uint64_t timestamp;
uint32_t event_id;
// ... potentially many other data fields
};
Write the function signatures (just the void name(...) part) for the following three functions:
- A function named
process_and_consume_eventthat needs to modify the event. The event is not optional. - A function named
render_event_detailsthat only needs to read the event data to display it. It must be as efficient as possible. - A function named
set_parent_eventthat links an event to an optional parent event. The parent event might not exist.
Show answer
// 1. Modifies the object, which must exist. Use a non-const reference.
void process_and_consume_event(TelemetryEvent& event);
// 2. Reads the object, should be efficient. Use a const reference.
void render_event_details(const TelemetryEvent& event);
// 3. The object is optional. Use a pointer.
void set_parent_event(TelemetryEvent* parent_event);
Conclusion
You've now covered one of the most fundamental and powerful features of C++. Organizing code into functions is essential, but understanding how data flows into them is what separates a basic programmer from a systems programmer.
Key Takeaways:
- Pass-by-value creates a copy, which is safe but can be slow for large objects.
- Pass-by-pointer passes a memory address, allowing for efficient modification of data but with more complex syntax and the risk of
nullptr. - Pass-by-reference provides a cleaner, safer syntax for the same indirection, acting as an alias for the original variable.
- Under the hood, references are often just compiler-managed pointers, giving you safety guarantees at compile time.
- The modern C++ idiom is to pass by
constreference for large, read-only data to get the best of both safety and performance.
In our next lesson, we will dive deeper into grouping related data using structs and classes, and defining member functions. You've seen the TelemetryEvent struct in the exercise; soon, you'll be building it and giving it its own behavior, laying the groundwork for the core data structures of your telemetry engine.