Beginner's Guide
    First Steps
    Input & Output
    Basic Custom Types
    Diagnostics
    Standard Library
    Code Organization
    Powerful Custom Types
    Generic Programming
    Memory Management
    Software Design Basics

    ReferencesReferencesReferences

    Capabilities (& Limitations) Capabilities

    non-const References non-const

    int   i = 2;
    int& ri = i;  // reference to i
    ri and i refer to the same object / memory location:
    cout << i  <<'\n';   // 2
    cout << ri <<'\n';   // 2
    
    i = 5; cout << i <<'\n'; // 5 cout << ri <<'\n'; // 5
    ri = 88; cout << i <<'\n'; // 88 cout << ri <<'\n'; // 88
    • references cannot be "null", i.e., they must always refer to an object
    • a reference must always refer to the same memory location
    • reference type must agree with the type of the referenced object
    int  i  = 2;
    int  k  = 3;
    int& ri = i;    // reference to i
    ri = k;         //  COMPILER ERROR: reference can't be redirected
    
    int& r2; // COMPILER ERROR: reference must be initialized double& r3 = j; // COMPILER ERROR: types must agree

    const References const&

    = Read-Only Access To An Object
    int i = 2;
    int const& cri = i;  // const reference to i
    • cri and i refer to the same object / memory location
    • but const means that value of i cannot be changed through cri
    cout << i   <<'\n';   // 2
    cout << cri <<'\n';   // 2
    
    i = 5; cout << i <<'\n'; // 5 cout << cri <<'\n'; // 5
    cri = 88; // COMPILER ERROR: const!

    auto References auto&

    reference type is deduced from right hand side of assignment
    auto i = 2;           // i: int
    auto d = 2.023;       // d: double
    auto x = i + d;       // x: double
    auto & ri = i;        // ri:  int &
    auto const& crx = x;  // crx: double const&

    Usage

    References in Range-Based for Loops In Range-Based for Loops Range-Based for

    std::vector<std::string> v;
    v.resize(10);
    
    // modify vector elements: for(std::string & s : v) { cin >> s; } // read-only access to vector elements: for(std::string const& s : v) { cout << s; }
    // modify: for(auto & s : v) { cin >> s; } //read-only access: for(auto const& s : v) { cout << s; }

    const Reference Parameters const& Parameters const Parameters

    Read-Only Access ⇒ const&
    • avoids expensive copies
    • clearly communicates read-only intent to users of function

    only needs to read values from vector!

    pass by value ⇒ copy
    int median(vector<int>);
    
    auto v = get_samples("huge.dat"); auto m = median(v); // runtime & memory overhead!
    pass by const& ⇒ no copy
    int median(vector<int> const&);
    
    auto v = get_samples("huge.dat"); auto m = median(v); // no copy ⇒ no overhead!

    augmented({1,2,4},{6,7,8,9}) → {1,2,4,6,9}

    The implementation works on a local copy 'x' of the first vector and only reads from the second vector via const reference 'y':

    auto augmented(  std::vector<int> x,   std::vector<int> const& y) {
      if(y.empty() return x;
      // append to local copy 'x'
      x.push_back(y.front());
      x.push_back(y.back());
      return x;
    }

    non-const Reference Parameters non-const Ref. Parameters non-const Parameters

    Example: Function that exchanges values of two variables

    void swap(int& i, int& j) {
      int temp = i;  // copy value: i → temp// copy i's value to temp
      i = j;         // copy value: j → i// copy j's value to i
      j = temp;      // copy value: temp → j// copy temp's (i's original value) to j
    }
    
    int main() { int a = 5; int b = 3; swap(a,b); cout << a << '\n' // 3 << b; // 5 }

    Use std::swap to exchange values of objects (#include <utility>).

    It can be used like the function above, but avoids expensive temporary copies for move-enabled objects like std::vector (it's implementation will be explained in chapter Move Semantics).

    As useful as non-const references might be in some situations, you should avoid such output parameters in general (see the next panels for more details).


    Function Parameters: copy / const& / &? Parameters: copy / const& / &? copy / const& / &?

    void read_from (int);  // fundamental types
    void read_from (std::vector<int> const&);
    void copy_sink (std::vector<int>);
    void write_to  (std::vector<int> &);
    Read from cheaply copyable object (all fundamental types) ⇒ pass by value
    double sqrt(double x) { … }
    Read from object with larger (> 64bit) memory footprint ⇒ pass by const&
    void print(std::vector<std::string> const& v) {
      for(auto const& s : v) { cout << s << ' '; }
    }
    Copy needed inside function anywaypass by value

    Pass by value instead of copying explictly inside the function. The reasons for this will be explained in more advanced articles.

    auto without_umlauts(std::string s) {
      s.replace('ö', "oe");  // modify local copy
      
      return s;  // return by value!
    }
    Write to function-external object ⇒ pass by non-const&

    As useful as they might be in some situations, you should avoid such output parameters in general, see here why.

    void swap(int& x, int& y) { … }

    Avoid Output Parameters! Avoid non-const refs!

    Functions with non-const ref parameters like
    void foo(int, std::vector<int>&, double);

    can create confusion/ambiguity at the call site:

    foo(i, v, j);
    • Which of the arguments (i, v, j) is changed and which remains unchanged?
    • How and when is the referenced object changed and is it changed at all?
    • Does the reference parameter only act as output (function only writes to it) or also as input (function also reads from it)?

    ⇒ in general hard to debug and to reason about!

    Example: An interface that creates nothing but confusion
    void bad_minimum(int x, int& y) {
      if(x < y) y = x;
    }
    
    int a = 2; int b = 3; bad_minimum(a,b); // Which variable holds the smaller value again?

    Binding Rules Binding

    Rvalues vs. Lvalues R/Lvalues

    Lvalues = expressions of which we can get memory address
    • refer to objects that persist in memory
    • everything that has a name (variables, function parameters, …)

    Rvalues = expressions of which we can't get memory address
    • literals (123, "string literal", …)
    • temporary results of operations
    • temporary objects returned from functions
    int a = 1;      // a and b are both lvalues
    int b = 2;      // 1 and 2 are both rvalues
    a = b;
    b = a;
    
    a = a * b; // (a * b) is an rvalue int c = a * b; // OK
    a * b = 3; // COMPILER ERROR: cannot assign to rvalue
    std::vector<int> read_samples(int n) { … } auto v = read_samples(1000);

    Reference Binding Rules Rules

    & only binds to Lvalues
    const& binds to const Lvalues and Rvalues
    bool is_palindrome(std::string const& s) { … }
    
    std::string s = "uhu"; cout << is_palindrome(s) <<", " << is_palindrome("otto") <<'\n'; // OK, const&
    void swap(int& i, int& j) { … }
    
    int i = 0; swap(i, 5); // COMPILER ERROR: can't bind ref. to literal

    Pitfalls

    Never Return A Reference To A Function-Local Object! Don't Return Refs To Locals! Ref Returning

    int& increase(int x, int delta) {
        x += delta;
        return x;
    }  //  local x destroyed
    
    int main() { int i = 2; int j = increase(i,4); // accesses invalid reference! }
    Only valid if referenced object outlives the function!
    int& increase(int& x, int delta) {
        x += delta;
        return x;  // x references non-local int
    }  // OK, reference still valid
    
    int main() { int i = 2; int j = increase(i,4); // OK, i and j are 6 now }

    Careful With Referencing vector Elements! Careful With vector Elements! Careful With vector!

    References to elements of a std::vector might be invalidated after any operation that changes the number of elements in the vector!
    vector<int> v {0,1,2,3};
    int& i = v[2];
    v.resize(20);  
    i = 5; //  UNDEFINED BEHAVIOR: original memory might be gone!
    • Dangling Reference = Reference that refers to a memory location that is no longer valid.

    The internal memory buffer where std::vector stores its elements can be exchanged for a new one during some vector operations, so any reference into the old buffer might be dangling.

    Avoid Lifetime Extension!

    References can extend the lifetime of temporaries (rvalues)
    auto const& r = vector<int>{1,2,3,4};

    ⇒ vector exists as long as reference r exists

    What about an object returned from a function?
    std::vector<std::string> foo() { … }

    take it by value (recommended):
    vector<string> v1 = foo();  
    auto v2 = foo();

    ignore it ⇒ gets destroyed right away
    foo();

    get const reference to it ⇒ lifetime of temporary is extended

    … for as long as the reference lives

    vector<string> const& v3 = foo();  
    auto const& v4 = foo();

    don't take a reference to its members!

    No lifetime extension for members of returned objects (here: the vector's content)!

    string const& s = foo()[0];  // dangling reference!
    cout << s;                   //  UNDEFINED BEHAVIOR
    Don't use lifetime extension through references!
    • easy to create confusion
    • easy to write bugs
    • no real benefit

    Just take returned objects by value. This does not involve expensive copies for most functions and types in modern C++, especially in C++17 and above.