Skip to content

References

Declaring a named variable as a reference, i.e., an alias for an existing object or function.

References can be thought of as C++'s encapsulated non-null pointers, used to refer to the objects they point to, and must be bound to an object upon declaration.

References are not objects; therefore, there are no arrays of references, pointers to references cannot be obtained, and there are no references to references.

Reference types are not object types

If you want references to support general operations like copying and assignment, such as being used as container elements, you need reference_wrapper, which typically maintains a non-null pointer.

References are mainly divided into two types: lvalue references and rvalue references.

Lvalues and rvalues

For an explanation of lvalues and rvalues, refer to the Value Category page.

Lvalue Reference T&

The references we commonly encounter are lvalue references, i.e., references bound to lvalues. Additionally, const-qualified lvalue references can bind to rvalues. The following is an example code snippet from the reference manual.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>
#include <string>

int main() {
  std::string s = "Ex";
  std::string& r1 = s;
  const std::string& r2 = s;

  r1 += "ample";  // Modifies r1, i.e., modifies s
  // r2 += "!"; // Error: cannot modify through a const reference
  std::cout << r2 << '\n';  // Prints r2, accesses s, outputs "Example"
}

Lvalue references are most commonly used as function parameters to avoid unnecessary copying.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <string>

// s in the parameter is a reference; no copy occurs when calling the function
char& char_number(std::string& s, std::size_t n) {
  s += s;  // 's' is the same object as 'str' in main(),
           // also demonstrating that lvalues can appear on the right side of assignment
  return s.at(n);  // string::at() returns a reference to char
}

int main() {
  std::string str = "Test";
  char_number(str, 1) = 'a';  // The function returns an lvalue, which can be assigned to
  std::cout << str << '\n';   // Outputs "TastTest"
}

Rvalue Reference T&& (C++11)

Rvalue references are references bound to rvalues, used for moving objects and can also be used to extend the lifetime of temporary objects.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>

using namespace std;

int main() {
  string s1 = "Test";
  // string&& r1 = s1; // Error: cannot bind to lvalue; requires std::move or static_cast

  const string& r2 = s1 + s1;  // Valid: lvalue reference to const extends lifetime
  // r2 += "Test"; // Error: cannot modify through a const reference
  cout << r2 << '\n';

  string&& r3 = s1 + s1;  // Valid: rvalue reference extends lifetime
  r3 += "Test";
  cout << r3 << '\n';

  const string& r4 = r3;  // Rvalue reference can convert to const-qualified lvalue reference
  cout << r4 << '\n';

  string& r5 = r3;  // Rvalue reference can convert to lvalue reference
  cout << r5 << '\n';
}

Dangling References

When the object a reference refers to is destroyed, the reference becomes a dangling reference. Accessing a dangling reference is undefined behavior and may cause program crashes.

Common examples of dangling references include:

  • Reference to a local variable

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    #include <iostream>
    
    int& foo() {
      int a = 1;
      return a;
    }
    
    int main() {
      int& b = foo();
      std::cout << b << std::endl;  // Undefined behavior
    }
    
  • Dangling reference due to deallocation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <iostream>
    
    int main() {
      int* ptr = new int(10);
      int& ref = *ptr;
      delete ptr;
    
      std::cout << ref << std::endl;  // Undefined behavior
    }
    
  • Dangling reference due to memory reallocation

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    #include <iostream>
    
    int main() {
      std::string str = "hello";
    
      const char& ref = str.front();
    
      str.append("world");  // May reallocate memory, causing the memory pointed to by ref to be freed
    
      std::cout << ref << std::endl;  // Undefined behavior
    }
    

    Similar to std::vector, insertion operations in containers like std::unordered_map may also cause memory reallocation.

When using references, always be mindful of the lifetime of the object the reference points to to avoid creating dangling references.

Static analysis tools and good coding practices can help us avoid dangling reference issues.

Eliminating Copy Overhead for Non-Trivial Object Parameters

Common non-trivial objects include:

  • Containers like vector, array, map, etc.
  • string
  • Other types that implement or inherit custom copy constructors, move constructors, or other special functions

Using references for trivial objects offers no benefit; the size of a reference type as a parameter may even be larger than the type itself.

This can introduce performance overhead and may prevent compiler optimizations.

The following are considered trivial objects:

  • Fundamental types like int, float, etc.
  • Small aggregate types
  • Standard library container iterators

Converting Lvalues to Rvalues

Use std::move to transfer ownership of an object. This is commonly seen between local variables or between parameters and local variables:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <string>
#include <vector>

using namespace std;

string world(string str) { return std::move(str) += " world!"; }

int main() {
  // 1
  cout << world("hello") << '\n';

  vector<string> vec0;

  // 2
  {
    string&& size = to_string(vec0.size());

    size += ", " + to_string(size.size());

    vec0.emplace_back(std::move(size));
  }

  cout << vec0.front();
}

However, this is not always necessary, such as in return value optimization.

Extending Temporary Lifetime with Rvalue References

Semantically, temporaries may cause additional copies or moves. Although compilers can often optimize these away via copy elision, references can force the compiler to avoid these redundant operations, eliminating uncertainty.

References

  1. C++ Language Documentation—Reference Declaration
  2. C++ Language Documentation—Value Category
  3. Does const ref lvalue to non-const func return value specifically reduce copies?