References

Capabilities (& Limitations)

non-const References

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; // assigns value of k to i (target of ri)
int& r2; // ❌ COMPILER ERROR: reference must be initialized
double& r3 = i; // ❌ COMPILER ERROR: types must agree

const References

= 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

reference type is deduced from right hand side of assignment

int i = 2;           
double d = 2.023;
double x = i + d;
auto & ri = i; // ri: int &
auto const& crx = x; // crx: double const&

Binding Rules

Rvalues vs. 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

& 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!

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!

⚠ 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 () { … }
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.

References

https://hackingcpp.com/cpp/lang/references.html