Advanced C++

Pointers and references

Máté Cserép

October 2015, Budapest

Overview

  • Scope and Life Rules
  • Connection between pointers and arrays
  • Parameter passing by value and by reference
  • Return type by value and by reference
  • Reference to class members
  • Reference and temporary objects
  • Implementing reference with proxy objects
  • Pointers, references and polymorphism
  • Null pointer, null reference?
  • Left and right values
  • Right value references
  • Move semantics

The concept

In modern programming languages there are two important concepts defining the behaviour of variables.

  1. Scope: Defines the area in the program source where a certain identifier is bound to a memory location.
    Also defines the visibility: when the identifier is valid to use and means the memory location we mentioned before.
  2. Life: Defines the timespan under runtime when the memory location is safe to store our values. After the duration the memory location is not safe to access.

The concept

The concept

// 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;

Scope and Life Rules

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

Connection between pointers and arrays

// 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;
}

Connection between pointers and arrays

// 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[];

Parameter passing

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 type

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.

Returning reference

// 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;
}

Returning constant reference

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
}

Returning constant reference

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 members

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.

Temporary objects

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. 

  • non-const references can bind to lvalues,
  • const references can bind to lvalues or rvalues.

Proxy objects

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;
};

Proxy objects

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 ...
};

Proxy objects

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 ...
};  

Polymorphism

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

Null pointer

#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)

Null reference?

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.

lvalue and rvalue

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.

Left values

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

Right values

Performance problems

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.

  • For small arrays new and delete result poor performance: 1/10 of C
  • For medium arrays, overhead of extra loops and memory access add +50%
  • For large arrays, the cost of the temporaries are the limitations

Solution?

The pseudo-code above will produce 4 temporary objects, their values copied each time, generating a significant performance overhead.

Introducing a new reference type

  • It would be nice not to create the temporaries, but steal the resources of the arguments of operator+()
  • We should destroy only the temporaries and keep the original resources for the b, c, d, e variables.
  • How can we distinguish between variables and unnamed temporaries?

Overloading

  • We need a separate type!
    What kind of requirements we have for this type?
    1. should be a reference type - otherwise we gain nothing
    2. if there is an overload between ordinary reference and this new type then rvalues should prefer the new type and lvalues should prefer the ordinary reference

rvalue references

  • This new type of reference will be called rvalue reference: X&&
    Note: X is not a template, which case will be discussed later.
  • The old type of reference will be called lvalue reference: X&
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.

rvalue references with OOP

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.

rvalue references with OOP

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.

Reverse compatibility

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.

Special member function generation

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.

Special member function generation

  1. The two copy operations are independent. Declaring copy constructor does not prevent compiler to generate copy assignment and vice versa.
    (Same as in C++98)
  2. Move operations are not independent. Declare either prevents the compiler to generate the other. (Different from copy operations.)
  3. If any of the copy operations is declared, then none of the move operation will be generated.
  4. If any of the move operation is declared, then none of the copy operation will be generated. This is the opposite rule of the previous.
  5. If a destructor is declared, then none of the move operation will be generated. Copy operations are still generated for reverse compatibility with C++98.
  6. Default constructor generated only when no constructor is declared. 
    (Same as in C++98)

Default generation

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 ... */

Move semantics

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
}

Move semantics and STL

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:

  • fstream
  • unique_ptr (non-shared, non-copyable)
  • thread
ifstream  myfile("myfile.txt");
// ...
ifstream  current_file = myfile;  // no copy, file handler is passed

Move semantics and inheritance

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

Is the move semantics safe?

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

Reference collapsing

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:

Advanced C++: Pointers and references

By Cserép Máté

Advanced C++: Pointers and references

  • 273