Video Coming Soon...
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 likestd::stringit 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
resultparameter. But, it can't becauseresultis copied to thebad_adder(). That means any changes thatbad_adder()makes are thrown away when the function exits. They are local tobad_adder(). print_result("bad_adder", 10, 20, err, result)- To prove this, I print out everything from this run. Notice how
resultdidn't change? err = bad_adder(-10, 20, result)- This runs it again but this time
-10is used so you can see that theerrreturn works, it's just theresultnever 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
errdoes. err = error_adder(24, 14, result)- The
error_adder()works as expected becauseresultis defined withint& result, which means it is a reference to anint, not a copy of it. What does that mean? At this exact point in the code you have aresultvariable that lives inmain(). Whenerror_adder()is called the compiler tells it, "Hey, whenever you changeresult, change the one that lives inmain()." Every timeerror_adder()changes result the calculations are actually being directed atmain()'s copy ofresult. 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
errandresultchanged.
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
- Go back through previous exercises and try using references for the function parameters to see if the code changes.
- Also try adding
constto your references to see if that makes any difference. - Find places I used
&(ampersand) and remove them to see if that also causes any problems. - Find any place I use
constand see if removing that also has any impact.
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.