Search

7.3 — Passing arguments by reference

While pass by value is suitable in many cases, it has a couple of limitations. First, when passing a large struct or class to a function, pass by value will make a copy of the argument into the function parameter. In many cases, this is a needless performance hit, as the original argument would have sufficed. Second, when passing arguments by value, the only way to return a value back to the caller is via the function’s return value. While this is often suitable, there are cases where it would be more clear and efficient to have the function modify the argument passed in. Pass by reference solves both of these issues.

Pass by reference

To pass a variable by reference, we simply declare the function parameters as references rather than as normal variables:

When the function is called, ref will become a reference to the argument. Since a reference to a variable is treated exactly the same as the variable itself, any changes made to the reference are passed through to the argument!

The following example shows this in action:

This program is the same as the one we used for the pass by value example, except foo’s parameter is now a reference instead of a normal variable. When we call addOne(value), ref becomes a reference to main’s value variable. This snippet produces the output:

value = 5
value = 6

As you can see, the function changed the value of the argument from 5 to 6!

Returning multiple values via out parameters

Sometimes we need a function to return multiple values. However, functions can only have one return value. One way to return multiple values is using reference parameters:

This function takes one parameter (by value) as input, and “returns” two parameters (by reference) as output. Parameters that are only used for returning values back to the caller are called out parameters. We’ve named these out parameters with the suffix “out” to denote that they’re out parameters. This helps remind the caller that the initial value passed to these parameters doesn’t matter, and that we should expect them to be rewritten. By convention, output parameters are typically the rightmost parameters.

Let’s explore how this works in more detail. First, the main function creates local variables sin and cos. Those are passed into function getSinCos() by reference (rather than by value). This means function getSinCos() has access to the actual sin and cos variables, not just copies. getSinCos() accordingly assigns new values to sin and cos (through references sinOut and cosOut respectively), which overwrites the old values in sin and cos. Main then prints these updated values.

If sin and cos had been passed by value instead of reference, getSinCos() would have changed copies of sin and cos, leading to any changes being discarded at the end of the function. But because sin and cos were passed by reference, any changes made to sin or cos (through the references) are persisted beyond the function. We can therefore use this mechanism to return values back to the caller.

This method, while functional, has a few minor downsides. First, the caller must pass in arguments to hold the updated outputs even if it doesn’t intend to use them. More importantly, the syntax is a bit unnatural, with both the input and output parameters being put together in the function call. It’s not obvious from the caller’s end that sin and cos are out parameters and will be changed. This is probably the most dangerous part of this method (as it can lead to mistakes being made). Some programmers and companies feel this is a big enough problem to advise avoiding output parameters altogether, or using pass by address for out parameters instead (which has a clearer syntax indicating whether a parameter is modifiable or not).

Personally, we recommend avoiding out parameters altogether if possible. If you do use them, naming out parameters (and output arguments) with an “out” suffix (or prefix) can help make it clear that the value might be modified.

Limitations of pass by reference

Non-const references can only reference non-const l-values (e.g. non-const variables), so a reference parameter cannot accept an argument that is a const l-value or an r-value (e.g. literals and the results of expressions).

Pass by const reference

As mentioned in the introduction, one of the major disadvantages of pass by value is that all arguments passed by value are copied into the function parameters. When the arguments are large structs or classes, this can take a lot of time. References provide a way to avoid this penalty. When an argument is passed by reference, a reference is created to the actual argument (which takes minimal time) and no copying of values takes place. This allows us to pass large structs and classes with a minimum performance penalty.

However, this also opens us up to potential trouble. References allow the function to change the value of the argument, which is undesirable when we want an argument be read-only. If we know that a function should not change the value of an argument, but don’t want to pass by value, the best solution is to pass by const reference.

You already know that a const reference is a reference that does not allow the variable being referenced to be changed through the reference. Consequently, if we use a const reference as a parameter, we guarantee to the caller that the function will not change the argument!

The following function will produce a compiler error:

Using const is useful for several reasons:

  • It enlists the compilers help in ensuring values that shouldn’t be changed aren’t changed (the compiler will throw an error if you try, like in the above example).
  • It tells the programmer that the function won’t change the value of the argument. This can help with debugging.
  • You can’t pass a const argument to a non-const reference parameter. Using const parameters ensures you can pass both non-const and const arguments to the function.
  • Const references can accept any type of argument, including non-const l-values, const l-values, and r-values.

Rule

When passing an argument by reference, always use a const reference unless you need to change the value of the argument.

A reminder

Non-const references cannot bind to r-values. A function with a non-const reference parameter cannot be called with literals or temporaries.

References to pointers

It’s possible to pass a pointer by reference, and have the function change the address of the pointer entirely:

(We’ll show another example of this in the next lesson)

As a reminder, you can pass a C-style array by reference. This is useful if you need the ability for the function to change the array (e.g. for a sort function) or you need access to the array’s type information of a fixed array (to do sizeof() or a for-each loop). However, note that in order for this to work, you explicitly need to define the array size in the parameter:

This means this only works with fixed arrays of one particular length. If you want this to work with fixed arrays of any length, you can make the array length a template parameter (see chapter 13).

Pros and cons of pass by reference

Advantages of passing by reference:

  • References allow a function to change the value of the argument, which is sometimes useful. Otherwise, const references can be used to guarantee the function won’t change the argument.
  • Because a copy of the argument is not made, pass by reference is fast, even when used with large structs or classes.
  • References can be used to return multiple values from a function (via out parameters).
  • References must be initialized, so there’s no worry about null values.

Disadvantages of passing by reference:

  • Because a non-const reference cannot be initialized with a const l-value or an r-value (e.g. a literal or an expression), arguments to non-const reference parameters must be normal variables.
  • It can be hard to tell whether an argument passed by non-const reference is meant to be input, output, or both. Judicious use of const and a naming suffix for out variables can help.
  • It’s impossible to tell from the function call whether the argument may change. An argument passed by value and passed by reference looks the same. We can only tell whether an argument is passed by value or reference by looking at the function declaration. This can lead to situations where the programmer does not realize a function will change the value of the argument.

When to use pass by reference:

  • When passing structs or classes (use const if read-only).
  • When you need the function to modify an argument.
  • When you need access to the type information of a fixed array.

When not to use pass by reference:

  • When passing fundamental types that don’t need to be modified (use pass by value).

Rule

Use pass by (const) reference instead of pass by value for structs and classes and other expensive-to-copy types.


7.4 -- Passing arguments by address
Index
7.2 -- Passing arguments by value

171 comments to 7.3 — Passing arguments by reference

  • Cerezas

    In the paragraph "Pass by const reference", right after the sentence "Using const is useful for several reasons:", points 3 and 4 overlap.
    Those two points can be combined, for example to:
    "You cannot pass a const argument to a non-const reference parameter, whereas const references can accept any type of argument, including non-const l-values, const l-values and r-values."

  • mike

    in the last example, you use endl instead of "\n".

  • Yousuf

    Please correct me if I am wrong about the rationale behind using static with constexpr keywords before the variable pi here:

    We used the static keyword for two reasons mainly:
    01. pi has automatic duration. To make it static duration we used the `static` keyword. So now `pi` lives till the program's termination.
    02. We used `static` keyword along with `constexpr` to make sure `pi` is compile time constant and it is initialized only once.

    • Cerezas

      01. True.
      02. False: `constexpr` on its own makes sure `pi` is initialized at compile time and its value cannot change.

      Also remember the definition of initialization:
      "When a variable is defined, you can also provide an initial value for the variable at the same time. This is called initialization." (lesson 1.4)
      As a consequence, "initialized only once" does not make much sense, because a variable can be initialized at most once (definition being part of initialization).
      I think you meant "assigned a value" instead of "initialized".

  • Lucky Abby

    I don't understand this statement: As a reminder, you can pass a C-style array by reference.

    passing raw array to function is naturally by reference, then why

    what is primary purpose for this? is it to avoid decay to pointer?

    • nascardriver

      When you pass an array to a function the "regular" way

      it's passed by address and decays to a pointer. The function doesn't know the array's size and can't use range-based for loops.

      When you pass the array by reference, it stays an array and the function can use it as if it were declared inside the function.

  • Andreas Krug

    Shouldn't the last line of this lesson "Rule: Use pass by (const) reference instead of pass by value for structs and classes and other expensive-to-copy types." be placed in a beautiful green Rule box?

  • sAmIra

    Would you please tell me if there is anything wrong with the following code?

    • nascardriver

      You have a bunch of unused includes. Only include what you need. Line 8 is unnecessary, because you're overriding `ref` in line 9.

    • Gustavo Guedes

      Hi sAmIra! The problem with your code is regarding the scope of "int x". When the program returns from "foo", the address that stores the value of x does not belong to the scope of the process anymore. For more information please check https://github.com/google/sanitizers/wiki/AddressSanitizerUseAfterReturn

    • Pitfalls

      Remember that x dies at line 11. So I believe the pointer will contain garbage information when it returns.

  • constexpr

    As one of your "best parctice"'s rules, shouldn't we avoid magic numbers like '180.0'? shouldn't it be defined as constexpr?

    • nascardriver

      180.0 is used as a part of a mathematical formula, there is no sensible name for 180.0 and would make the code any clearer.

  • Antonio

    In the following code, if I pass c by const reference, add it to m_components vector, when it goes out of scope(c) in let's say the previous function, what would happen to the variable inside the vector?
    I thought that it would point to nonexistent memory but the list is correct even when the variable passed goes out of scope eventually?

    • nascardriver

      Assuming `m_components` is a `std::vector`, it stores elements by value. When you `push_back(c)`, a copy of `c` is created and the lifetime of `c` doesn't matter anymore. You'd only have a dangling problem if you stored references (eg. `std::reference_wrapper`) or pointers in the vector.

  • choofe

    Thank you.
    guess I should have another pass on chapter 6 and 7 again (3rd time).

  • marius

    This nomenclature is hilarious. We do not talk about constants and variables, we talk about constant variables and non constant variables ... It is a variable, you should not mention it's constantness.

    It's like we define constant as being a not variable variable and then define a variable as being a variable constant. LOL

    Bjarne was drunk back then. Sorry.

    • Petter Nybråten

      What's your suggestion, then? I see your point, but for someone making a programming language, isn't it natural to name entities that hold single values by one term? Whether they are subject to change or not? Does any other programming language call variables and constants something else, distinguish between them at the fundamental level?

  • marius

    Just to illustrate the cost of passing by value large objects many times. This takes 2 min and 37 seconds on my dual core E7500 machine. If you pass the array by reference, it takes no time to finish the execution.

  • choofe

    Hi. Can't we use "const int *& ptr" as const by ref by pointer parameter? And I don't get the error in the foo function.
    what is wrong with this:

    • Ahmed

      In the "foo" function, you are assigning a constant pointer reference(b) to a non-constant pointer reference(ptr).
      Constants cannot be assigned to non-const, but the reverse is OK.

      • choofe

        Thank you.
        I am still confused!
        As "The address-of operator returns a pointer" in lesson 6.7, I expected that I can send a pointer argument (&y) in:

        to a function that takes pointer parameter by ref:

        The by ref here is a way of treating the pointer argument which it would be reference not a copy. what do I miss? because this can't be done!

        I did my intention by defining a pointer and assign (&y) to it and then send it to foo().

        and this is OK.

        I almost get it that it is because &y is an r-value and pt is an l-value. This is C++ rule and I get it.
        but don't understand what harm is in that(sending an address of directly to a parameter)?

Leave a Comment

Put all code inside code tags: [code]your code here[/code]