In most common programming languages, passing arguments to functions is trivial, and can be done without much thought. C++, however, gives us the ability to specify exactly how to pass an argument to a function - not only that, but it forces us to specify it.

Some examples of different parameter types:

struct X {};

// pass by value
void fn1(int a, int b);

// pass by const reference
void fn2(const X& a);

// pass by (mutable) reference
void fn3(X& a);

// pass by value (but const)
void fn4(const int a);

// pass by pointer
void fn5(int* a);


Pass by value

For a parameter of a function, we definitely need a type. This can be any type that we use for variables, too, like int, std::vector<>, or any struct, class, primitive, etc.

If we only specify the type, such as

  • (int x)
  • (std::vector<int> v)

then we pass by value. This means that the value we give the function in this parameter is copied in its entirety (either by invoking the copy constructor or simply copying the value for trivially copyable types).

When we pass an int, float, size_t, or similar primitive type, to a function, a copy is usually what we want. This is because copying an integer, for example, is incredibly cheap for the computer. An int is often 4 bytes, which is very little data to copy.

On the other hand, if we have a std::vector with 1000 integers, and we copy it, the computer has to do a lot more work:

  1. The new (copy) std::vector asks the operating system for enough memory to hold 1000 integers.
  2. All values from the old vector are copied to the new vector (to be fair, this is usually optimized to be very fast)

This isn’t super expensive, but unless we really need a copy, we probably shouldn’t make one. Some objects, depending on the size and kind of data they hold, are incredibly expensive to copy, though.

For cases like std::vector, and any other class- or struct-type, we need a better way.

Pass by reference

Instead of giving our function a copy of the std::vector, we can tell it where to find the existing vector instead.

A common analogy is visiting a house. Imagine you’d like to visit my house, and look at it, maybe to buy it. I have two options (in this contrived analogy):

  1. I can send workers and materials over to where you are, and have them build a copy of the house for you, so you can look at it and see if you like it. Granted, you could also set it on fire and it wouldn’t affect my real house, but it’s super expensive.
  2. I can give you the address of my real house, so you can come over and visit it. I risk that you may set it on fire, but there are other ways to protect against that.

Option 1. is “pass-by-value”, and option 2 is “pass-by-reference”. Translating this analogy back to C++, we can just give the function the “address” of the std::vector. Since it’s C++, we won’t actually use the memory address (though we could), but instead we use a reference.

It looks like this:

// pass-by-value (makes a copy every time it's called)
void fn1(std::vector<int> myvec);

// pass-by-reference (takes a reference)
void fn2(std::vector<int>& myvec);

Now, fn2 can be called with a vector as the argument, and it won’t copy the vector. The issue from option 2 still exists, though - what if fn2 changes the myvec vector? It could even delete all data in it!

Pass by const-reference

Going back to the analogy, in “pass-by-const-reference”, I give you the address of my house, but also a contract we both sign, in which you promise you wont destroy or alter my house in any way.

This contract is const:

// pass-by-reference (may modify the vector)
void fn1(std::vector<int>& myvec);

// pass-by-const-reference (may not modify the vector)
void fn2(const std::vector<int>& myvec);

In both cases we only pass the “address”, a reference, but in fn2 we make it const. This way, fn2 gets the vector “for cheap” (no copy), but also can’t modify it. Win-win!

So what do I use?

Sadly, this is where the factual statements end - it’s all down to your use-case from here. Here are a few questions to help you pick which one you should use:

  • Do you need to modify the parameter inside the function?
    • “Yes”: Pass by reference (&).
    • “No”: Is it a primitive type (like int)?
      • “Yes”: Pass by value
      • “No”: Pass by const reference (const &).
  • Are you sure what you need now?
    • “Yes”: Great!
    • “No”: Pass by const reference (const &).

TL;DR: Always default to const &, and then see how it goes from there.

Note: There is also the option of passing by pointer, which is the same as passing by reference, except:

  • It can be nullptr, so you need to check for that.
  • Don’t use it unless you know for sure you need a pointer. Keep in mind you can take the address of a const &, if you need a pointer inside the function.