Video Coming Soon...

Created by Zed A. Shaw Updated 2026-04-30 02:05:52

22: Pass by Value, Pass by Reference

You've seen me create functions that have parameters like this:

void some_func(const std::string& name)

I've promised to explain this to you and this is the exercise where you learn what this means. The quick explanation is this:

When you append & (ampersand) after a type like std::string it says you want to reference to the original variable, rather than a copy. This lets your function modify the original source variable.

To better understand this concept we'll make a little test case and explore how it works.

The Code

As usual, get this code to run and then I'll break it down:

View Source file ex22.cpp Only

#include <fmt/core.h>

using fmt::println;

int bad_adder(int a, int b, int result) {
  if(a < 0 || b < 0) {
    return -1;
  } else {
    result = a + b;
    return 0;
  }
}

int error_adder(int a, int b, int& result) {
  if(a < 0 || b < 0) {
    return -1;
  } else {
    result = a + b;
    return 0;
  }
}

void print_result(const std::string& func_name, int a, int b, int err, int result) {
  println("{}: a={},b={}, err={}, result={}", func_name, a, b, err, result);
}


int main(int argc, char* argv[]) {
  int result = 0;
  int err = 0;

  err = bad_adder(10, 20, result);
  print_result("bad_adder", 10, 20, err, result);

  err = bad_adder(-10, 20, result);
  print_result("bad_adder", -10, 20, err, result);

  err = error_adder(24, 14, result);
  print_result("error_adder", 24, 14, err, result);

  err = error_adder(-24, 14, result);
  print_result("error_adder", -24, 14, err, result);
}

Try to figure out what the & (ampersand) is doing before you read the breakdown.

Code Breakdown

For this code breakdown I'm going to talk about each function separately instead of individual lines. There's nothing new here, just the & usage. First, let's look at the bad_adder() function:

int bad_adder(int a, int b, int result) {
  if(a < 0 || b < 0) {
    return -1;
  } else {
    result = a + b;
    return 0;
  }
}

This function is an example of a common idiom in languages like C where the return value is used as an error indicator. The intention here is to use the return to signal an error, but to place the result into the last parameter with int result. C uses this because it doesn't have Exceptions, and variations of it show up in many C like languages for a similar reason.

The advantage of doing this is mostly to avoid exceptions or simply because you're using a C library that does it. In embedded systems you don't have the space to use Exceptions, and some people avoid them for other reliability reasons.

The problem with this function--thus why it's called bad_adder()--is that the int result is a copy not a reference. What that means is when the function is called main() the result variable in main() is actually copied onto the stack, and then bad_adder() works on that copy.

For this to work you need to add the & (ampersand) to the int so that the compiler knows you want a reference, not a copy:

int error_adder(int a, int b, int& result) {
  if(a < 0 || b < 0) {
    return -1;
  } else {
    result = a + b;
    return 0;
  }
}

This variation will actually work because you've told the compiler you want a reference to result with int& result. When you have this the code result = a + b is then assigning straight to the result variable in main(), instead of to a copy that's local to the function.

Now that I've explained these two functions we can look at each line of the main() function to better understand what's going on:

int result = 0
We need a variable for the result.
int err = 0
Another variable for the errors.
err = bad_adder(10, 20, result)
This is the "bad" version of our functions, which is trying to return an error and also change the result parameter. But, it can't because result is copied to the bad_adder(). That means any changes that bad_adder() makes are thrown away when the function exits. They are local to bad_adder().
print_result("bad_adder", 10, 20, err, result)
To prove this, I print out everything from this run. Notice how result didn't change?
err = bad_adder(-10, 20, result)
This runs it again but this time -10 is used so you can see that the err return works, it's just the result never gets set because it's a copy.
print_result("bad_adder", -10, 20, err, result)
Again, print this out so you can see it didn't work, but err does.
err = error_adder(24, 14, result)
The error_adder() works as expected because result is defined with int& result, which means it is a reference to an int, not a copy of it. What does that mean? At this exact point in the code you have a result variable that lives in main(). When error_adder() is called the compiler tells it, "Hey, whenever you change result, change the one that lives in main()." Every time error_adder() changes result the calculations are actually being directed at main()'s copy of result.
print_result("error_adder", 24, 14, err, result)
This prints the results to show you it does work.
err = error_adder(-24, 14, result)
This does it again, but now you get the error since -24 is used.
print_result("error_adder", -24, 14, err, result)
Once again, prove that it worked by printing. In this case, you see both the err and result changed.

Discussion

This way of using references is fairly rare in C++. I'm only using it here as a demonstration that you can understand. A more generic reason for doing this is to treat a parameter as an "out parameter," which means that you'll be placing a result value in the parameter. This also isn't very common in modern C++ because there's other APIs you can use to do multiple returns, and you can usually return multiple values easily in other ways.

A primary reason references are used in C++ is for speed and memory savings. Copying an int isn't too big of a deal and usually will be faster and take less memory than a reference to an int. If it's a larger structure--like say a std::string or std::ifstream--then you definitely don't want to copy it to the function you call. A reference will make sure that only an amount of data needed for the reference is used, not an entire std::string.

A secondary reason for references is when you actually don't want to copy some resource because there really is only one. If you had opened a std::ifstream then that is attached to a single file, so making a copy of it could cause problems when there's two different copies trying to work on the same file. With a reference you know that only one is being used which keeps the file sane.

Finally, you may be wondering why const is frequently added when using a & reference. It's because C++ programmers want to have their cake and eat it too. They want to pass a reference so a copy isn't created, but also don't want the function to make changes to it. The word const means constant, so think of it as saying, "Here's a reference, but don't touch it! You don't own it!"

Mostly...like many things in C++, this is mostly true.

The Practice

You should now attempt each of these to become more solid in your abilities:

Change It
Take this code and change it in some way. Remember, the changes don't have to be amazing. Any simple change will do.
Break It
Try to find novel ways to break this code. Try some syntax errors, feeding it bad data, etc.
Recreate It
Write a description of this code in your own words, then try to recreate it from your description. Remember that you can always look at your original if you get stuck, but hide away again after you take a peek.

Further Study

Previous Lesson Next Lesson

Register for Learn C++ the Hard Way

Register to gain access to additional videos which demonstrate each exercise. Videos are priced to cover the cost of hosting.