References References References
int i = 2;
int& ri = i; // reference to i
ri
and i
refer to the same object / memory location:
#include <iostream>
int main () {
using std::cout;
int i = 2;
int& ri = i;
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
= Read-Only Access To An Object
int i = 2;
int const& cri = i; // const reference to i
cri
andi
refer to the same object / memory location- but
const
means that value ofi
cannot be changed throughcri
#include <iostream>
int main () {
using std::cout;
int i = 2;
int const& cri = i;
cout << i <<'\n'; // 2
cout << cri <<'\n'; // 2
i = 5;
cout << i <<'\n'; // 5
cout << cri <<'\n'; // 5
cri = 88; // COMPILER ERROR: const!
}
#include <iostream>
int main () {
using std::cout;
int i = 2;
double d = 2.023;
double x = i + d;
auto & ri = i; // ri: int &
auto const& crx = x; // crx: double const&
std::cout << ri << '\n';
std::cout << crx << '\n';
}
Usage
References in Range-Based for
Loops
In Range-Based for
Loops
Range for
#include <iostream>
#include <string>
#include <vector>
int main () {
std::vector<std::string> v;
v.resize(10);
// modify vector elements:
std::cout << "enter " << v.size() << " words (separated by spaces):";
for (std::string & s : v) { std::cin >> s; }
// read-only access to vector elements:
for (std::string const& s : v) { std::cout << s << '\n'; }
std::cout << '\n';
// modify:
std::cout << "enter " << v.size() << " words (separated by spaces):";
for (auto & s : v) { std::cin >> s; }
//read-only access:
for (auto const& s : v) { std::cout << s << '\n'; }
std::cout << '\n';
}
const
Reference Parameters
const&
Parameters
const Parameters
Read-Only Access ⇒ const&
- avoids expensive copies
- clearly communicates read-only intent to users of function
Example: Function that computes median Example: median
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!
Example: Mixed passing Example: Mixed passing (by ref + by value)
incl_first_last ({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 incl_first_last ( 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
#include <iostream>
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);
std::cout << a << '\n' // 3
<< b << '\n'; // 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).
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 anyway ⇒ pass 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) { … }
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?
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);
& |
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
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.
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.
Comments…