References
Lvalue References
Lippman:
- Ordinarily, when we initialize a variable, the value of the initializer is copied into the object we are creating
- When we define a reference, instead of copying the initializer’s value, we bind the reference to its initializer
- Once initialized, a reference remains bound to its initial object
- There is no way to rebind a reference to refer to a different object
- Because there is no way to rebind a reference, references must be initialized
Liberty:
- idea
- Normally, when you use a reference, you do not use the address-of operator. You simply use the reference as you would use the target variable.
- references are aliases for their target
- passing by reference enables the function to change the object being referred to
- Normally, when you use a reference, you do not use the address-of operator. You simply use the reference as you would use the target variable.
- references (unlike other variables)
- must be initialized when they are declared
- cannot be reassigned
space before the address-of operator is required- wrong: see counterexample from cppreference:
// https://en.cppreference.com/w/cpp/language/reference_initialization
// - shows that the space before the "&" is NOT required
double d = 2.0;
double& rd = d; // rd refers to d
const double& rcd = d; // rcd refers to d
Address of a Reference
Liberty:
- If you ask a reference for its address, it returns the address of its target.
// address of a reference
int intOne;
int &rSomeRef = intOne;
intOne = 5;
// intOne: 5
// rSomeRef: 5
// &intOne: 0x3500
// &rSomeRef: 0x3500
- from isocpp:
- Unlike a pointer, once a reference is bound to an object, it can not be “reseated” to another object.
- The reference itself isn’t an object
- it has no identity
- taking the address of a reference gives you the address of the referent
- remember: the reference is its referent
Initialization
Lippman:
- usually “the type of a reference must match the type of the object to which it refers”
- two exceptions:
- 1) “we can initialize a reference to
const
from any expression that can be converted to the type of the reference” (see reference to const) - 2) “classes related by inheritance”
- 1) “we can initialize a reference to
- two exceptions:
- a reference may be bound only to an object, not to a literal or to the result of a more general expression:
int &refVal4 = 10; // error: initializer must be an object
double dval = 3.14;
int &refVal5 = dval; // error: initializer must be an int object
Reference to Reference
What does this mean?: “Because references are not objects, we may not define a reference to a reference.”, (Lippman, p51)
From stackoverflow:
// https://stackoverflow.com/q/28359555/12282296
int main(){
int ival=1024;
int &refVal=ival;
int &refVal2=refVal;
return 0;
}
After refVal
is initialized, whenever you mention its name, it behaves like the variable ival
it refers to—its “referenceness” can no longer be detected (except by decltype
). Therefore refVal2
is simply initialized to refer to ival
also.
There is no type “reference to reference to int
”, int&(&)
.
reference to const
- aka “
const
reference”, “lvalue reference to aconst
value” X const& x
is equivalent toconst X& x
, andX const* x
is equivalent toconst X* x
. (see isocpp.org)- Unlike an ordinary reference, a reference to
const
cannot be used to change the object to which the reference is bound - “we can initialize a reference to
const
from any expression that can be converted to the type of the reference”- we can bind a reference to
const
to- a nonconst object,
- a literal, or
- a more general expression
- an object of a different type (in this case, the reference is bound to a temporary object, see p.62)
- we can bind a reference to
- “Binding a reference to
const
to an object says nothing about whether the underlying object itself isconst
.”- ie. the underlying object cannot be changed by using the reference to
const
, but it might be changed by other means
- ie. the underlying object cannot be changed by using the reference to
- binding a non-const reference to a temporary is illegal in C++, only const references may be bound to a temporary (see p.62)
const int ci = 1024;
const int &r1 = ci; // ok: both reference and underlying object are const
r1 = 42; // error: r1 is a reference to const
int &r2 = ci; // error: nonconst reference to a const object
// Initialization and References to const:
int i = 42;
const int &r1 = i; // we can bind a const int& to a plain int object
const int &r2 = 42; // ok: r1 is a reference to const
const int &r3 = r1 * 2; // ok: r3 is a reference to const
int &r4 = r * 2; // error: r4 is a plain, non const reference
// Temporaries and References to const:
// see examples in section "Temporaries"
// A Reference to const May Refer to an Object That Is Not const:
int i = 42;
int &r1 = i; // r1 bound to i
const int &r2 = i; // r2 also bound to i; but cannot be used to change i
r1 = 0; // r1 is not const; i is now 0
r2 = 0; // error: r2 is a reference to const
const references
- usually refers to reference to const
From isocpp.org:
- references are always
const
X& const x
is functionally equivalent toX& x
. Since you’re gaining nothing by adding theconst
after the&
, you shouldn’t add it: it will confuse people - theconst
will make some people think that theX
is const, as if you had saidconst X& x
.
Classes Related By Inheritance
Lip15.2.3:
- we can bind a pointer or reference to a base-class type to an object of a type derived from that base class
- examples:
- use a
Quote&
to refer to aBulk_quote
object - assign the address of a
Bulk_quote
object to aQuote*
- use a
- consequently, when we use a reference (or pointer) to a base-class type, we don’t know the actual type of the object to which the pointer or reference is bound
- That object can be an object of the base class or it can be an object of a derived class
- the smart pointer classes support this “derived-to-base conversion”, too
Rvalue References
- “are primarily intended for use inside classes” (Lip2.3.1)
- introduced by the new standard to support move operations
- must be bound to an rvalue
- obtained by using
&&
- may be bound only to an object that is about to be destroyed
- so that we are free to “move” resources from an rvalue reference to another object
- rvalue and lvalue references have the opposite binding properties:
int i = 42;
int &r = i; // ok: r refers to i
int &&rr = i; // error: cannot bind an rvalue reference to an lvalue
int &r2 = i * 42; // error: i * 42 is an rvalue
const int &r3 = i * 42; // ok: we can bind a reference to const to an rvalue
int &&rr2 = i * 42; // ok: bind rr2 to the result of the multiplication
Examples of expressions that return
- lvalues
- Functions that return lvalue references
- operators
- assignment
- subscript
- dereference
- prefix increment/decrement (
++i
)
- Variable expressions
- rvalues (prvalues)
- Functions that return a nonreference type
- operators
- arithmetic
- relational
- bitwise
- postfix increment/decrement (
i++
)
- literals
- rvalue references refer to objects that are about to be destroyed
- ephemeral vs persistent:
- lvalues have persistent state, whereas rvalues are either literals or temporary objects
- code that uses an rvalue reference is free to take over value/resources/state from the object to which the reference refers
- “steal” value/resources/state from the object (rhs) bound to an rvalue reference (lhs)
- you cannot directly bind an rvalue reference to a variable, even if that variable was defined as an rvalue reference type
int &&rr1 = 42; // ok: literals are rvalues
// Error:
int &&rr2 = rr1; // error: the expression rr1 is an lvalue!
Reference to Pointer
From stackoverflow:
- a reference to a pointer is like a reference to any other variable
void fun(int*& ref_to_ptr)
{
ref_to_ptr = 0; // set the "passed" pointer to 0
// if the pointer is NOT passed by ref,
// then only the copy(parameter) you received is set to 0,
// but the original pointer(outside the function) is not affected.
}
This is often useful when returning a pointer type data member:
// whereas, if we would return the data member `_p` "by value", we
// cannot modify it (eg. to make `_p` point to another address)
class X {
double* _p{nullptr};
public:
double*& p() { // return by reference
return _p;
}
}
Pointer to Reference
From stackoverflow:
- A pointer to reference is illegal in C++
- because - unlike a pointer - a reference is just a concept that allows the programmer to make aliases of something else.
- A pointer is a place in memory that has the address of something else, but a reference is NOT.
Forwarding Reference
- The term universal reference was coined by Scott Meyers as a common term that could result in either an lvalue reference or an rvalue reference.
- The C++17 standard introduced the term forwarding reference, because the major reason to use such a reference is to forward objects.
- However, note that it does not automatically forward.
- The term does not describe what it is but what it is typically used for.
Forwarding Reference vs Rvalue Reference
X&&
for a specific type X
- declares a parameter to be an rvalue reference
- it can only be bound to a movable object (a prvalue, such as a temporary object, and an xvalue, such as an object passed with
std::move()
)- see move constructor and move-assignment operator
- it is always mutable
- you can always “steal” its value
T&&
for a template parameter T
- declares a forwarding reference (also called universal reference)
- it can be bound to a mutable, immutable (i.e.,
const
), or movable object- see
f(T&& val)
in Perfect Forwarding
- see
- it is mutable or immutable
const
is never dropped!
- you can sometimes “steal” its value
remove_reference_t
Problem: References must be initialized. Therefore, the following function template itself won’t work properly with lvalue arguments.
template<typename T> void f(T&& p) // p is a forwarding reference
{
T x; // for passed lvalues, x is a reference
...
}
Solution: To deal with this situation, the std::remove_reference
type trait is frequently used to ensure that x
is not a reference
template<typename T> void f(T&& p) // p is a forwarding reference
{
std::remove_reference_t<T> x; // x is never a reference
...
}
Perfect Forwarding
- to write generic code that “forwards” the basic property of passed arguments
- modifiable objects forwarded as modifiable
- preserve constness
- movable objects forwarded as movable
- called perfect forwarding, because the result of calling
g()
indirectly throughf()
(orforwardToG()
respectively) will be the same as if the code calledg()
directly - no additional copies are made
Example with Naive Forwarding
Forward a call of f()
to a corresponding function g()
:
Without templates, we would have to program all three cases for f()
separately:
#include <utility>
#include <iostream>
class X {
...
};
void g(X&) {
std::cout << "g() for variable\n";
}
void g(X const&) {
std::cout << "g() for constant\n";
}
void g(X&&) {
std::cout << "g() for movable object\n";
}
// let f() forward argument val to g():
void f(X& val) {
g(val); // val is non-const lvalue => calls g(X&)
}
void f(X const& val) {
g(val); // val is const lvalue => calls g(X const&)
}
void f(X&& val) {
g(std::move(val)); // val is non-const lvalue => needs std::move() to call g(X&&)
// without move() g(X&) would be called
}
int main()
{
X v; // create variable
X const c; // create constant
f(v); // f() for nonconstant object calls f(X&) => calls g(X&)
f(c); // f() for constant object calls f(X const&) => calls g(X const&)
f(X()); // f() for temporary calls f(X&&) => calls g(X&&)
f(std::move(v)); // f() for movable variable calls f(X&&) => calls g(X&&)
}
Example with Perfect Forwarding
The same using perfect forwarding to forward arguments:
#include <utility>
#include <iostream>
class X {
...
};
void g(X&) {
std::cout << "g() for variable\n";
}
void g(X const&) {
std::cout << "g() for constant\n";
}
void g(X&&) {
std::cout << "g() for movable object\n";
}
// let f() perfect forward argument val to g():
template<typename T>
void f(T&& val) {
g(std::forward<T>(val)); // call the right g() for any passed argument val
}
int main()
{
X v; // create variable
X const c; // create constant
f(v); // f() for variable calls f(X&) => calls g(X&)
f(c); // f() for constant calls f(X const&) => calls g(X const&)
f(X()); // f() for temporary calls f(X&&) => calls g(X&&)
f(std::move(v)); // f() for move-enabled variable calls f(X&&) => calls g(X&&)
}
Forwarding Arguments using static_cast<T&&>
Forward the argument along to another function g()
:
- problem: the expression
x
will always be an lvalue (recall: variables are lvalues) - solution: the
static_cast
castsx
to its original type and lvalue- or rvalue-ness, thereby achieving perfect forwarding
class C {
...
};
void g(C&);
void g(C const&);
void g(C&&);
template<typename T>
void forwardToG(T&& x)
{
g(static_cast<T&&>(x)); // forward x to g()
}
void foo()
{
C v;
C const c;
forwardToG(v); // eventually calls g(C&)
forwardToG(c); // eventually calls g(C const&)
forwardToG(C()); // eventually calls g(C&&)
forwardToG(std::move(v)); // eventually calls g(C&&)
}
Forwarding Arguments using std::forward<T>
best practice:
- use
std::forward<>()
(in header<utility>
) instead ofstatic_cast
for perfect forwarding- better documents the programmer’s intent
- prevents errors (such as omitting one
&
)
#include <utility>
template<typename T>
void forwardToG(T&& x)
{
g(std::forward<T>(x)); // forward x to g()
}
// std::forward<T> is essentially a static_cast<T&&>
// (but it is more convenient than a static_cast<T&&>)
template <typename T>
T&& forward(std::remove_reference_t<T>& t) {
return static_cast<T&&>(t);
}
Thus, preserving type information is a two-step process:
- To preserve type information in the arguments, we must define
forwardToG
’s function parameters as rvalue references to a template type parameterT&&
. - We must use
forward
to preserve the arguments’ original types whenforwardToG
passes those arguments tog
.