Skip to main content

Welcome to a Grasp lesson

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

Lesson 4

Functions: Value, Reference, and Pointer Parameters

Recommended resources

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:

  1. Pass-by-value
  2. Pass-by-pointer
  3. 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:

  1. Return type: The type of the value the function sends back (e.g., int, or void if it returns nothing).
  2. Function name: An identifier for the function.
  3. Parameters: A list of variables that the function accepts as input.
  4. 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.

C++ Function Parameters: Pass by Value, Reference, and Pointer
This image provides a quick visual summary of the syntax for the three parameter-passing mechanisms we will explore: by value, by reference, and by pointer.

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.

Memory Address and Content for Different Data Types
This diagram illustrates how variables are stored in memory. Each variable (like `i`, `s`, `d`) has a value and a unique hexadecimal address. A pointer, like `iptr`, is a special variable whose value is the address of another variable.

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 be nullptr (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.

what even is a "reference"?

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:

  1. 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 struct or class)? -> Pass by const reference.
        // Efficient and safe: No copy is made, and the function can't change my_event.
        void log_event(const TelemetryEvent& my_event);
        
    • Yes (modification is intended):
      • Does the object have to exist (is null not 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 null a 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);
        

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:

  1. A function named process_and_consume_event that needs to modify the event. The event is not optional.
  2. A function named render_event_details that only needs to read the event data to display it. It must be as efficient as possible.
  3. A function named set_parent_event that 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 const reference 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.