Pass by Value and Reference

Pass by Value

In the last section, we saw how to use objects to pass arguments to functions and return multiple values. Let’s look into that some more. We will be using the some similar code, but we’ll make some slight modifications.

  1. #include <stdio.h>
  2. struct counter {
  3. int loops;
  4. };
  5. void loop(counter x) {
  6. x.loops++;
  7. }
  8. int main() {
  9. counter t;
  10. t.loops = 0;
  11. while(1) {
  12. loop(t);
  13. printf("Looped %d times \n", t.loops);
  14. }
  15. }

When we run this code we notice something odd. The loop function is supposed to directly modify the object x on line 8. And maybe some of us will expect that the object t on line 15 will be modified, and so on line 16 t.loops should have changed. But it didn’t, and this is important to note. If we pass a object to a function, everything is just copied. It’s the same situation as when we pass an int or a char or some other primitive data type. This copy-everything-when-a-function-is-called behavior is called pass by value. In C++, everything is pass by value by default.

In other programming languages, you will find that when you make objects and pass those to a function, they won't completely make a clone of the object and they’ll just pass the object itself. The behavior of dont-copy-things-just-give-the-original is called pass by reference. In pass by reference, an object modified inside a function will be modified outside as well, in pass by value, you’re only modifying the copy and not the original.

Let’s show an experiment to verify that we are indeed copying everything completely.

  1. #include <stdio.h>
  2. struct counter {
  3. int data[1000000];
  4. };
  5. void loop(counter x) {
  6. }
  7. int main() {
  8. counter t;
  9. int loops = 0;
  10. while(1) {
  11. loop(t);
  12. printf("Looped %d times \n", ++loops);
  13. }
  14. }

You can observe how slow this code runs visually. Then try changing the size of data into something small. Make it an array of size 10 for example, then re-compile it and run. You should notice a clear difference in how fast the loop on line 13 to 15 can iterate. This is an effect of the need to copy four megabytes of data every time a counter is passed to a function. (4-byte int times one million elements = four megabytes)

It’s important to keep in mind the advantages and disadvantages of pass by value: copying objects takes time proportional to the size of the object in bytes, and you cannot modify the original objects if you only have a copy in the first place.

Passing Pointers

Since everything in C++ is pass by value, is there anything we can do if we want to modify objects inside the functions we pass them to? Is there any way we can save time from copying? As you may already have figured out, the answer is on the title of this section. Just pass pointers to the objects!

Since this is a fairly common trick, let me show you some sample code.

  1. #include <stdio.h>
  2. struct counter {
  3. int data[1000000];
  4. int loops;
  5. };
  6. void loop(counter* x) {
  7. (*x).loops++;
  8. }
  9. int main() {
  10. counter t;
  11. t.loops = 0;
  12. while(1) {
  13. loop(&t);
  14. printf("Looped %d times \n", t.loops);
  15. }
  16. }

Yay! Now this one is just as fast as if we had nothing to copy, because we aren’t copying anything anymore aside from the pointer. Even better, we can now modify the object inside the function it’s passed into. The only bad thing is now we have to wrap all the member access with (*x).member_name instead of just saying x.member_name. The good news is that C++ has a shorthand for this since this situation is so common. The shorthand is using the arrow (->) operation instead of dot (.). Let’s look at some more sample code.

  1. #include <stdio.h>
  2. struct counter {
  3. int data[1000000];
  4. int loops;
  5. };
  6. void loop(counter* x) {
  7. x->loops++;
  8. }
  9. int main() {
  10. counter t;
  11. t.loops = 0;
  12. while(1) {
  13. loop(&t);
  14. printf("Looped %d times \n", t.loops);
  15. }
  16. }

So it’s the exact same code as last time but we used an arrow on line 9 instead of the star and dot. So that’s the shorthand for accessing members of struct pointers.

Pass by Reference in C++

Before we end this lesson, we’ll demonstrate how to pass by reference in C++ without "cheating" by passing a pointer.

  1. #include <stdio.h>
  2. struct counter {
  3. int data[1000000];
  4. int loops;
  5. };
  6. void loop(counter& x) {
  7. x.loops++;
  8. }
  9. int main() {
  10. counter t;
  11. t.loops = 0;
  12. while(1) {
  13. loop(t);
  14. printf("Looped %d times \n", t.loops);
  15. }
  16. }

If you look carefully on line 8, there’s an ampersand (&) after the data type. That marks the variable as pass by reference and the compiler will automatically do all the work of the pointer stuff in the background. Once this gets compiled, the resulting program would turn out more or less the same as if we had done the pointer stuff manually. Personally, I dislike pass by reference in C++ because it acts against your expectations if you accidentally miss reading that small & character. Normally, you would expect that if you pass an argument to a function, that variable you gave won’t be modified. But modifying the passed variable is exactly what pass by reference is for.

Avoiding the use of pass by reference really only a matter of opinion and style and there are many common libraries that heavily use it. The input reading object cin is one of those that heavily use pass by reference.