Máté Cserép
October 2015, Budapest
In modern programming languages there are two important concepts defining the behaviour of variables.
// allocates memory for an int type variable (in the stack)
// binds the name "i" to this memory area.
int i;
We can define scope and life in the same declaration:
We can define a memory location, without binding a name to it:
// allocates memory for an int type variable (in the heap)
// no name has been bound to this memory
new int;
We can bind a new name to an existing memory location, whether a name has been already bound to it or not:
// no memory allocation
// binds the name "j" to memory area already called "i"
int &j = i;
We can also store the address of a memory location as the value of another:
// allocates memory for an int* type variable (in the stack) with the value of
// i's memory address binds the name "p" to this memory area.
int *p = &i;
void f()
{
int i; // start of scope and life i
int &ir = i; // start of scope ir, ir bound to i
ir = 6; // ok
} // end of life i, end of scope i and ir
A normal variable has scope and life, a reference only has scope:
Which can lead to problems:
void g()
{
int *ip = new int; // start of life *ip
int &ir = *ip; // start of scope ir, ir bound to *ip
delete ip; // end of life *ip
*ip = 6; // bad
ir = 6; // bad
} // end of scope ip and ir
// a.cpp
int t[] = { 1, 2, 3, 4, 5 };
void f(int *a);
void g();
int main()
{
int *p = t;
std::cout << "t = " << std::hex << t << std::endl; // 0x404010
std::cout << "p = " << std::hex << p << std::endl; // 0x404010
std::cout << "sizeof(t) = " << sizeof(t) << std::endl; // 14 (impl. dependant)
std::cout << "sizeof(p) = " << sizeof(p) << std::endl; // 8 (impl. dependant)
std::cout << "t[2] = " << t[2] << std::endl; // ok: 3
std::cout << "p[2] = " << p[2] << std::endl; // ok: 3
f(t); // definition in b.cpp
f(p); // definition in b.cpp
g(); // definition in b.cpp
++p; // ok, p can be lvalue
++t; // syntax error, t cannot be lvalue
return 0;
}
// b.cpp
extern int *t;
void f(int *a)
{
std::cout << "a = " << std::hex << a << std::endl; // 0x404010
std::cout << "sizeof(a) = " << sizeof(a) << std::endl; // 8 (impl. dependant)
std::cout << "a[2] = " << a[2] << std::endl; // ok: 3
}
void g()
{
std::cout << "t = " << std::hex << t << std::endl; // 0x200000001
std::cout << "t[2] = " << t[2] << std::endl; // undefined behaviour
std::cout << "sizeof(t) = " << sizeof(t) << std::endl; // undefined behaviour
}
Both declarations must have compatible types, and int* is not compatible with int[].
What actually happens is undefined, the first sizeof(int*) bytes of the array can be interpreted as an address.
// b.cpp
extern int t[];
Parameters can be passed by address or by value.
The former just uses equivalent memory fields for the formal and the actual parameter, but the latter copies the value of actual parameter into a local variable in the area of the subprogram.
int i = 6;
int j = 42;
void f(int x, int y) {
// ...
}
void g(int &x, int &y) {
// ...
}
f(i, j); // ==> int x = i; // creates local x and copies i to x (copy constructor semantic)
// int y = j; // creates local y and copies j to y (copy constructor semantic)
g(i, j); // ==> int &x = i; // binds x as a new name to existing i
// int &y = j; // binds y as a new name to existing j
What happens here?
Return value can also be passed by address or by value.
The meaning of former is to bind the function expression to the returning object, while with the latter the returning object is copied from the function to the target.
int f() // returns by value
{
int i; // local variable with automatic storage
//...
return i; // return by value
}
int& g() // returns by reference
{
int i; // local variable with automatic storage
//...
return i; // returns the reference
}
int j = f(); // ok: value of i has copied into j
int& k = g(); // bad: no copy, k refers to invalid memory location
When returning object will not survive the function call expression we must copy it.
// file my_complex.h
class my_complex
{
public:
my_complex(double r, double i);
double& real() { return r; }
double& imaginary() { return i; }
my_complex& add(double r, double i);
my_complex& operator++();
my_complex operator++(int);
private:
double r;
double i;
};
// file my_complex.cpp
#include "my_complex.h"
my_complex::my_complex(double r, double i)
: r(r), i(i) { }
// returns reference the original object
my_complex& my_complex::add(double r, double i) {
this->r += r;
this->i += i;
return *this;
}
// returns reference to the original
// (incremented) object
my_complex& my_complex::operator++() {
r += 1;
return *this;
}
// returns value with copy of the
// temporary object before incrementation
my_complex my_complex::operator++(int) {
my_complex orig(*this);
r += 1;
return orig;
}
// file main.cpp
int main()
{
my_complex c(6, 5);
double i = c.imaginary();
c.real() = 8;
++(c.add(3, 4));
my_complex d = c++;
return 0;
}
template <typename T>
class matrix {
public:
// ... other parts omitted ...
// This function returns reference to the selected value,
// allowing clients to modify the appropriate element of the matrix.
T& operator()(int i, int j) { return v[i*cols+j]; }
// This const member function must return const reference,
// otherwise the const-correctness would be leaking.
const T& operator()(int i, int j) const { return v[i*cols+j]; }
// Most assignment operators are defined with (non-const) reference type as return value.
// This is more effective than a value returning type, and also enables method-call chaining.
matrix& operator+=(const matrix& other) {
for (int i = 0; i < cols*rows; ++i)
v[i] += other.v[i];
return *this;
}
private:
// ... other parts omitted ...
T* v;
};
template <typename T>
matrix<T> operator+(const matrix<T>& left, const matrix<T>& right) {
matrix<T> result(left); // local variable: automatic storage
result += right;
return result; // result will disappear: must copy
}
matrix<double> dm(10,20);
// ... fill dm with values ...
dm(2,3) = 3.14; // modify matrix element
cout << dm(2,3); // copies matrix element
dm(2,3) += 1.1; // modify matrix element
double& dr = dm(2,3); // doesn't copy
dr += 1.1; // modify matrix element
const matrix<double> cm = dm;
cm(2,3) = 3.14; // syntax error: returns with const reference
cout << cm(2,3); // ok: copies (read) matrix element
cm(2,3) += 1.1; // syntax error: returns with const reference
double& dr = cm(2,3); // syntax error: const reference does not convert to reference
const double& cdr = cm(2,3); // ok: doesn't copy
Usage of the previously defined matrix class:
Usage for constant instances:
class X
{
public:
X(const std::string& name) : _name(name) { }
std::string& name() { return _name; }
const std::string& name() const { return _name; }
private:
std::string _name;
};
While returning references to class members - or to other self contained values - can be efficient in contrast to return by value, it also has a downside.
X *p = new X("Mate");
const X *cp = p;
const std::string &name = cp->name();
std::cout << name << std::endl; // ok
delete p;
std::cout << name << std::endl; // possible segmentation fault: dangling reference
Similar issue holds with dangling pointers when returning pointers.
int f() { return 42; } // returns by value
Const references can bind to temporary objects.
const int& cir = f(); // ok: const ref to temporary object
int& ir = f(); // snytax error: non-const ref to temporary object
Normally, a temporary object lasts only until the end of the full expression in which it appears. However, C++ deliberately specifies that binding a temporary object to a reference to const on the stack lengthens the lifetime of the temporary to the lifetime of the reference itself, and thus avoids what would otherwise be a common dangling-reference error.
matrix<int> m(10,20);
m[2][3] = 1;
std::cout << m[2][3] << std::endl;
Reference as a concept could be implemented by the programmer too. As an example the desired functionality below cannot be easily implemented, because operator[]() only exists with one parameter.
Therefore we create a proxy class:
template <class T> class matrix;
template <class T>
class proxy_matrix
{
public:
proxy_matrix(matrix<T> *m, int i) : mptr(m), row(i) { }
T& operator[](int j) { return mptr->at(row,j); }
const T& operator[](int j) const { return mptr->at(row,j); }
private:
matrix<T> *mptr;
int row;
};
template <class T>
class matrix
{
public:
proxy_matrix<T> operator[](int i)
{
return proxy_matrix<T>(this,i);
}
T& at(int i, int j) { return v[i*x+j]; }
const T& at(int i, int j) const { return v[i*x+j]; }
T& operator()(int i, int j) { return v[i*x+j]; }
const T& operator()(int i, int j) const { return v[i*x+j]; }
private:
int x;
int y;
T *v;
};
const matrix<int> cm = m;
std::cout << cm[2][3] << std::endl; // syntax error
Unfortunately we would still have a problem with constant matrix objects:
We must create a second helper as the return value of the constant version of the indexer operation:
template <class T> class matrix;
template <class T>
class proxy_matrix
{
public:
proxy_matrix(matrix<T> *m, int i) : mptr(m), row(i) { }
T& operator[](int j) { return mptr->at(row,j); }
private:
matrix<T> *mptr;
int row;
};
template <class T>
class const_proxy_matrix
{
public:
const_proxy_matrix(const matrix<T> *m, int i) : mptr(m), row(i) { }
const T& operator[](int j) { return mptr->at(row,j); }
private:
const matrix<T> *mptr;
int row;
};
template <class T>
class matrix
{
// ... unchanged parts omitted ...
proxy_matrix<T> operator[](int i)
{
return proxy_matrix<T>(this,i);
}
const_proxy_matrix<T> operator[](int i) const
{
return const_proxy_matrix<T>(this,i);
}
// ... unchanged parts omitted ...
};
matrix<int> *p = new matrix<int>(10,20);
matrix<int> &r = *p;
proxy_matrix<int> pm = r[2];
delete p;
std::cout << pm[3] << std::endl; // causes runtime error
Now the usage of proxy objects are a bit too dangerous:
We should forbid the creation and permanent storage of proxy objects.
In fact, that is more what the built-in references does.
template <class T>
class matrix
{
private:
class proxy_matrix
{
friend proxy_matrix matrix<T>::operator[] (int i);
public:
T& operator[](int j) { return mptr->at(row,j); }
private:
proxy_matrix(matrix<T> *m, int i) : mptr(m), row(i) { }
proxy_matrix(const proxy_matrix& rhs);
void operator=(const proxy_matrix& rhs);
matrix<T> *mptr;
int row;
};
class const_proxy_matrix
{
friend const_proxy_matrix matrix<T>::operator[](int i) const;
public:
T operator[](int j) { return mptr->at(row,j); }
private:
const_proxy_matrix(const matrix<T> *m, int i) : mptr(m), row(i) { }
const_proxy_matrix(const const_proxy_matrix& rhs);
void operator=(const const_proxy_matrix& rhs);
const matrix<T> *mptr;
int row;
};
public:
// ... unchanged parts omitted ...
proxy_matrix operator[](int i)
{
return proxy_matrix(this,i);
}
const_proxy_matrix operator[](int i) const
{
return const_proxy_matrix(this,i);
}
// ... unchanged parts omitted ...
};
struct Base
{
virtual void test() {
std::cout << "Base" << std::endl;
}
};
struct Derived : Base
{
virtual void test() {
std::cout << "Derived" << std::endl;
}
};
void f(Base *base) { base->test(); }
void g(Base &base) { base.test(); }
void h(Base base) { base.test(); }
Derived d;
Derived *dp = &d;
Base *bp = dp;
f(dp); // Derived
f(bp); // Derived
g(d); // Derived
h(d); // Base
Polymorphism works the same both for pointers and referens
#define NULL ((void *)0) // can simply be 0 in compilers not following ANSI C
int x = NULL; // raises warning, make integer from pointer implicitly
ANSI C:
C++98/03:
#define NULL 0 // or 0L, since void* does not implicitly convert to other pointer types
void foo(int); // (1)
void foo(void*); // (2)
foo(NULL);
C++11:
#define NULL nullptr
int *p = nullptr; // ok
int i = nullptr; // syntax error
The type of nullptr is nullptr_t, which can be implicitly converted to and compared with other pointer types, but does not convert to integral types.
// calls (1)
Base *bp = new Base;
// null if dynamic_cast is invalid
if (Derived *dp = dynamic_cast<Derived*>(bp) )
{
// ...
}
else
{
// ...
}
Base b;
Base &br = b;
// throws exception if dynamic_cast is invalid
try
{
Derived &dr = dynamic_cast<Derived&>(br);
// ....
}
catch(std::bad_cast)
{
// ...
}
A key difference between pointers and references is that there is no such thing like null reference. A reference is always identifies a valid object in the memory.
In C/C++ there might be expressions on both side of an assignment, e.g.:
*++p = *++q; // likely ok if p is pointer, except p is const *
But not all kind of expressions may appear on the left:
y + 5 = x; // likely error except some funny operator+
An lvalue is an expression that refers to a memory location and allows us to take the address of that memory location via the & operator. An rvalue is an expression that is not an lvalue.
Note: in C++ this is a bit more complex with function lvalues since functions are not objects.
int i = 42;
int &j = i;
int *p = &i;
i = 99;
j = 88;
*p = 77;
int *fp() { return &i; } // returns pointer to i: lvalue
int &fr() { return i; } // returns reference to i: lvalue
*fp() = 66; // i = 66
fr() = 55; // i = 55
int f() { int k = i; return k; } // returns rvalue
i = f(); // ok
p = &f(); // bad: can't take address of rvalue
f() = i; // bad: can't use rvalue on left-hand side
extern matrix<double> a, b, c, d, e;
a = b + c + d + e;
C and C++ has value semantics: when we use assignment we copy by default.
This case has been investigated by Todd Veldhuizen and has led to C++ template
metaprogramming and expression templates.
Solution?
The pseudo-code above will produce 4 temporary objects, their values copied each time, generating a significant performance overhead.
Overloading
void f(X& arg) // lvalue reference parameter
void f(X&& arg) // rvalue reference parameter
X x;
X g();
f(x); // lvalue argument => f(X&)
f(g()); // rvalue argument => f(X&&)
X a;
X f();
X& r1 = a; // ok: bind to an lvalue
X& r2 = f(); // syntax error: can't bind to rvalue
X&& rr1 = f(); // ok: bind to a temporary
X&& rr2 = a; // syntax error: can't bind to lvalue
An rvalue reference (&&) can bind to an rvalue but not to an lvalue.
class X
{
public:
X() { /* ... */ }
~X() { /* ... */ }
X(const X& rhs);
X(X&& rhs);
X& operator=(const X& rhs);
X& operator=(X&& rhs);
private:
/* ... */
};
X::X(const X& rhs)
{
// copy resource from rhs
}
// draft version, will be revised later
X::X(X&& rhs)
{
// move resource from rhs
// leave rhs in a destructable state
}
X& X::operator=(const X& rhs)
{
// free old resources
// then copy resources from rhs
return *this;
}
// draft version, will be revised later
X& X::operator=(X&& rhs)
{
// free old resources
// then move resources from rhs
// leave rhs in a destructable state
return *this;
}
The move constructor and move assignment can and usually do modify their arguments. They are destructive.
class Y;
class X
{
public:
X() { _y = new Y; }
~X() { delete _y; }
X(const X& rhs);
X(X&& rhs);
X& operator=(const X& rhs);
X& operator=(X&& rhs);
private:
Y _y;
};
X::X(const X& rhs)
{
_y = new Y(rhs._y);
}
X::X(X&& rhs)
{
// no copy needed
_y = rhs._y;
rhs._y = 0;
}
X& X::operator=(const X& rhs)
{
delete _y;
_y = new Y(rhs._y);
return *this;
}
X& X::operator=(X&& rhs)
{
// no copy needed
delete _y;
_y = rhs._y;
rhs._y = 0;
return *this;
}
The move constructor and move assignment can and usually do modify their arguments. They are destructive.
If we implement the old-style member functions with lvalue reference parameters, but do not implement the rvalue reference overloading versions we should keep the old behaviour.
However, if we implement "only" the rvalue operations, than we cannot call these on lvalues. In this case no default lvalue copy constructor or operator= should be generated.
Move operations generated only if they are needed.
If they are generated, they perform member-wise moves. Move constructor also moves base part or non-static members.
Move operations are "move requests": if we want to move something not supporting move semantics (like C++98/03 classes), then it will "move by copy" silently.
If we would like to enforce the generation of the default copy or move operations, we can do it:
class X
{
public:
~X(); // user declared destructor -> denies generation of move operations
X(const X& rhs) = default; // to generate copy constructor
X(X&& rhs) = default; // to generate move constructor
X& operator=(const X& rhs) = default; // to generate copy assignment
X& operator=(X&& rhs) = default; // to generate move assignment
private:
/* ... */
};
/* ... implementation omitted ... */
template <class Expensive>
void swap(Expensive& a, Expensive& b)
{
Expensive tmp(a); // second copy of a
a = b; // second copy of b
b = tmp; // second copy of tmp
}
The copy by default semantics of C++ makes certain operations (much) more expensive, e.g.:
With std::move() we use move semantic on lvalue, it converts its argument to rvalue reference, does not do anything else. Specially, std::move() do nothing in run-time.
We should think of std::move() as "rvalue reference cast". Called move() for historical reasons, it would be better to called rval().
template <class Expensive>
void swap(Expensive& a, Expensive& b)
{
Expensive tmp = move(a); // no copy, but may invalidate a
a = move(b); // no copy, but may invalidate b
b = move(tmp); // no copy, but may invalidate tmp
}
std::vector<unique_ptr<Base>> v1, v2;
v1.push_back(unique_ptr<Base>(new Derived())); // move, no copy
v2 = v1; // syntax error: not copyable
v2 = move(v1); // ok: pointers are moved to v2
In C++11 all STL containers have move operations (copy constructor and assignment operator) and the insert new element operations have overloaded versions taking rvalue reference.
Note: for POD-style data, copy may be the most efficient.
There are movable but non-copyable types:
ifstream myfile("myfile.txt");
// ...
ifstream current_file = myfile; // no copy, file handler is passed
class Base
{
public:
Base(const Base& rhs); // non-move semantics
Base(Base&& rhs); // move semantics
};
class Derived : public Base
{
Derived(const Derived& rhs); // non-move semantics
Derived(Derived&& rhs); // move semantics
};
Derived::Derived(const Derived& rhs) : Base(rhs) // non-move semantics
{
// Derived-specific stuff
}
Derived::Derived(Derived&& rhs) : Base(std::move(rhs)) // good, calls Base(Base&& rhs)
{
// Derived-specific stuff
}
"If it has a name" rule
As we all know, the First Amendment to the C++ Standard states:
The committee shall make no rule that prevents C++ programmers from shooting themselves in the foot.
Popular joke cited by Thomas Becker on rvalue references
In pre-C++11 we have no way to set a reference to a reference: A& & is a compile error.
A& & | A& | |
A& && | A& | |
A&& & | A& | |
A&& && | A&& |
In C++11 we have a "reference collapsing" rule: