Advanced C++

Constructors, destructors

Máté Cserép

October 2015, Budapest

Overview

  • Creation and destruction time of storage classes
  • Explicit constructors, conversion operators
  • Polymorphism of arrays
  • Using constructors to create temporaries,
    lifetime of temporaries
  • Why use virtual destructors? When no to use them?
  • Cloning ("virtual constructors")
  • Default and deleted constructors
  • Delegating constructors
  • RVO (return value optimization)

Constructors

In object-oriented languages constructors have the role to initialize newly created objects. Different styles are used including factory methods.

 

Structures/classes without constructors can be instantiated without constructor call. If at least one constructor is defined for a class, objects must be created through one of the constructors

Explicit constructor

// file: date.h
class date
{
  friend bool operator<(date d1, date d2);
public:
  // constructor    
  date(int y=2000, int m=1, int d=1) : year(y), month(m), day(d) { }
  // explicit constructor
  explicit date(const char *s);
private:
  int year;
  int month;
  int day;
};

// question: use member or global operators?
bool operator<(date d1, date d2);
inline bool operator==(date d1, date d2) { return !(d1<d2 || d2<d1); }
inline bool operator!=(date d1, date d2) { return d1<d2 || d2<d1; }
inline bool operator<=(date d1, date d2) { return !(d2<d1); }
inline bool operator>=(date d1, date d2) { return !(d1<d2); }
inline bool operator>(date d1, date d2)  { return d2<d1; }

Explicit constructor

// file: date.cpp
date:: date(const char *s)
{
  sscanf(s, "%d.%d.%d", &year, &month, &day);
}

bool operator<(date d1, date d2)
{
  return d1.year < d2.year || d1.month < d2.month || d1.day < d2.day;
}
// file: main.cpp
int main()
{
  date d(2015, 3, 12);

  if (d < 2010) {                   // works: implicit constructor
    /* ... */
  }
  else if (d < "2015.1.12") {       // does not work: explicit constructor
    /* ... */
  }
  else if (d < date("2015.1.12")) { // works: explicit call of constructor 
    /* ... */
  }
}

Creation and destruction time

Named automatic object

  • Creation: when declaration is encountered
  • Destruction: when the program exits the block, in reverse order

Free store object

  • Creation: new
  • Destruction: delete

Non-static member object

  • Creation: when the object which is created, in the order of declaration
  • Destruction: when the object is destroyed, in reverse order

Array element

  • Creation: when the array is created, in growing index order
  • Destruction: when the array is destroyed, in reverse order

Creation and destruction time

Local static object

  • Creation: when first time the declaration is evaluated
  • Destruction: at the end of the program

​Global, namespace or class static object

  • Creation: at the start of the program
  • Destruction: at the end of the program

Temporary object

  • Creation: created as part of the evaluation of an expression
  • Destruction: at the end of the full expression

Placement new

  • Creation: new
  • Destruction: delete

Creation and destruction

Free store object:

date *p = new date;
date *q = new date(*p);
date *s = new date[10];

delete p;
delete p;   // undefined behaviour
delete s;   // undefined behaviour
class list
{
public:
   list();
  ~list();
  /* ... */
private:
  // The order is the relevant:
  int  id;
  list *next;
  list *prev;
};

// This will be automatically rearranged: id(nid), next(0), prev(0)
list::list() : id(nid), prev(0), next(0)
{ }

Creation of sub-objects:

Construction of statics

struct X
{
  X(int i)
  {
      x = i;
      std::cerr << "X ctor, x = " << x << std::endl;
  }
  int x;
};

void f()
{
  static X ix(0);
  std::cerr << "f(), ix.x = " << ix.x << std::endl;

  ++ix.x;
  if (ix.x > 5)
  {
    static X iy(ix.x);
    static X iz(ix);    // copy constructor
  }
}

int main()
{
  for ( int i = 0; i < 10; ++i) f();
  return 0;
}

Construction of arrays

struct X { X(int i) { x = i; }; int x; };
struct Y {                      int y; };

int main()
{
  const int n = 4;
        int k = 4;

  X x1[n];                // compile error: no default constructor
  X x2[n] = { 1, 2, 3, 4};
  Y y1[n];
  Y y2[k];                // compile error: k is non-const

  X *xp = new X[k];       // compile error: no default constructor
  Y *yp = new Y[k];

  std::vector<X> xv(10);  // compile error: no default constructor
  std::vector<Y> yv(10);

  delete [] yp;
  return 0;
}

Arrays are not polymorphic

struct Base
{
  Base()          { std::cout << "Base" << std::endl; }
  virtual ~Base() { std::cout << "~Base" << std::endl; }
  int i;
};

struct Derived : public Base
{
  Derived()          { std::cout << "Derived" <<std::endl; }
  virtual ~Derived() { std::cout << "~Derived" << std::endl; }
  int it[10];     // sizeof(Base) != sizeof(Derived) 
};

int main()
{
  Base *bp = new Derived;
  Base *bq = new Derived[5];

  delete    bp;
  delete[]  bq;   // this causes runtime error
  return 0; 
}

Temporaries

void f( string &s1, string &s2, string &s3)
{
  const char *cs = (s1+s2).c_str();
  cout << cs;           // OK or bad?

  if (strlen(cs = (s2+s3).c_str()) < 8 && cs[0] == 'a' )  // OK or bad?
    cout << cs;         // OK or bad?
}

Temporary objects:

  • Created under the evaluation of an expression
  • Destroyed when full expression has been evaluated
// Bad!!
// Bad!!
// OK

Temporaries

void f(string &s1, string &s2, string &s3)
{
    cout << s1 + s2;
    string s = s2 + s3;

    if (s.length() < 8 && s[0] == 'a' )
      cout << s;
}

The correct way:

Another correct way:

void f(string &s1, string &s2, string &s3)
{
    cout << s1 + s2;
    const string &s = s2 + s3;

    if (s.length() < 8 && s[0] == 'a' )
      cout << s;  // Ok
} // s1+s2 destroyes here, when the const ref goes out of scope

Virtual destructors

class Base
{
public:
  virtual ~Base() { std::cout << "~Base" << std::endl; }
};

class Derived : public Base
{
public:
  virtual ~Derived() { std::cout << "~Derived" << std::endl; }
};

int main()
{
  Base *bp = new Derived;
  delete bp;
  return 0;
}

In most cases destructors should be virtual:

"Virtual constructors"

class Base
{
public:
  Base()                { std::cout << "Base defCtor"  << std::endl; }
  Base(const Base& rhs) { std::cout << "Base copyCotr" << std::endl; }
  virtual Base* clone() { return new Base(*this); }
};

class Derived : public Base
{
public:
  Derived()                   { std::cout << "Derived defCtor"  << std::endl; }
  Derived(const Derived& rhs) : Base(rhs)
                              { std::cout << "Derived copyCotr" << std::endl; }
  virtual Derived* clone()    { return new Derived(*this); }
};

int main()
{
  Base *bp = new Derived;
  Base *bq = bp->clone();
  return 0;
}

There is no virtual constructor, but we can simulate it:

Deleted constructors

class X
{
  /* ... */
private:
  X& operator=(const X&);                // Disallow copying
  X(const X&);
};

Forbid copying

Alternatively we can use the boost library

class X : boost::noncopyable             // Disallow copying
{
  /* ... */
};

Since C++11 deleted functions are also usable

class X
{
  /* ... */
  X& operator=(const X&) = delete;      // Disallow copying
  X(const X&) = delete;
};

Default constructors

class Y
{
  /* ... */
  Y& operator=(const Y&) = default;     // Default copy semantics
  Y(const Y&) = default;
};

Conversely, we can also say explicitly that we want to use the default copy behaviour:

Delegating constructors

class Foo
{
public:
  Foo()
  {
    // code to do task A
  }

  Foo(int nValue)
  {
    // code to do task A
    // code to do task B
  }
};

In C++98/03, there are often cases where it would be useful for one constructor to call another constructor in the same class. Unfortunately, this is disallowed by C++03. Commonly this ends up resulting in either duplicated code:

Delegating constructors

class Foo
{
public:
  Foo()
  {
    InitA();
  }

  Foo(int nValue)
  {
    InitA();
    // code to do task B
  }

  void InitA()
  {
    // code to do task A
  }
};

Alternatively we van use a init() non-constructor function to keep the common code accessible to both constructors that need it:

Delegating constructors

class Foo
{
public:
  Foo()
  {
    // code to do task A
  }

  Foo(int nValue): Foo() // use Foo() default constructor to do A
  {
    // code to do task B
  }
};

Fortunately C++11 adds the ability to chain constructors together (called delegating constructors):

While using the init() method is considered better practice than duplicating code, it has a couple of downsides:

  • readability (new function and function calls),
  • additionally complexity (in order to handle both the new initialization and reinitialization cases properly).

Return Value Optimization

In general, the C++ standard allows a compiler to perform any optimization, provided the resulting executable exhibits the same observable behaviour as if (i.e. pretending) all the requirements of the standard have been fulfilled. This is commonly referred to as the "as-if" rule.

 

The term return value optimization (RVO) refers to a special clause in the C++ standard that goes even further than the "as-if" rule: an implementation may omit a copy operation resulting from a return statement, even if the copy constructor has side effects.

Return Value Optimization

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made." << std::endl; }
};
 
C f() {
  return C();
}
 
int main() {
  std::cout << "Hello World!" << std::endl;
  C obj = f();
  return 0;
}

The above example demonstrates a scenario where the implementation may eliminate one or both of the copies being made, even if the copy constructor has a visible side effect (printing text). The first copy that may be eliminated is the one where C() is copied into the function f()'s return value. The second copy that may be eliminated is the copy of the temporary object returned by f() to obj.

Return Value Optimization

Depending upon the compiler, and that compiler's settings, the resulting program may display any of the following outputs:

Hello World!
A copy was made.
A copy was made.
Hello World!
A copy was made.
Hello World!

May also optimize away move semantic!

Move semantics versus RVO

#include <iostream>
#include <algorithm>
#include <chrono>
#include <stdlib.h>
#include <time.h> 

struct Copyable
{
  static const int SIZE = 10000;

  Copyable()
  {
    data = new int[SIZE];
    for(int i = 0; i < sizeof(data); ++i)
      data[i] = rand();
  }
  virtual ~Copyable()
  {
    delete[] data;
  }

  Copyable(const Copyable& other)
  {
    data = new int[SIZE];
    std::copy(other.data, other.data + SIZE, data);
  }
  Copyable& operator=(const Copyable& other)
  {
    if(this == &other) return *this;
    delete[] data;
    data = new int[SIZE];
    std::copy(other.data, other.data + SIZE, data);
    return *this;
  }
protected:
  int *data;
};

struct Movable
{
  static const int SIZE = 10000;

  Movable()
  {
    data = new int[SIZE];
    for(int i = 0; i < sizeof(data); ++i)
      data[i] = rand();
  }
  virtual ~Movable()
  {
    delete[] data;
  }

  Movable(const Movable& other)
  {
    data = new int[SIZE];
    std::copy(other.data, other.data + SIZE, data);
  }
  Movable& operator=(const Movable& other)
  {
    if(this == &other) return *this;
    delete[] data;
    data = new int[SIZE];
    std::copy(other.data, other.data + SIZE, data);
    return *this;
  }

  Movable(Movable&& other)
  {
    data = other.data;
    other.data = 0;
  }
  Movable& operator=(Movable&& other)
  {
    if(this == &other) return *this;
    int *p = data;
    data = other.data;
    other.data = p;
    return *this;
  }
protected:
  int *data;
};

template <class T>
T generate()
{
  return T();
}

template <class T>
void swap_copy(T& a, T& b)
{
  T t(a);
  a = b;
  b = t;
}

template <class T>
void swap_move(T& a, T& b)
{
  T t(std::move(a));
  a = std::move(b);
  b = std::move(t);
}

template <class Function>
void measure(Function fun, const std::string& msg)
{
  std::chrono::high_resolution_clock::time_point start, end;
  start = std::chrono::high_resolution_clock::now();

  fun();

  end = std::chrono::high_resolution_clock::now();
  auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
  std::cout << msg << ": " << duration << "ms" << std::endl;
}

void test_copy_return()
{
  Copyable copy_obj;
  for(int i = 0; i < 100000; ++i)
    copy_obj = generate<Copyable>();
}

void test_move_return()
{
  Movable move_obj;
  for(int i = 0; i < 100000; ++i)
    move_obj = generate<Movable>();
}

void test_copy_swap()
{
  Copyable copy_obj_a, copy_obj_b;
  for(int i = 0; i < 100000; ++i)
    swap_copy(copy_obj_a, copy_obj_b);
}

void test_move_swap()
{
  Movable move_obj_a, move_obj_b;
  for(int i = 0; i < 100000; ++i)
    swap_move(move_obj_a, move_obj_b);
}

int main()
{
  srand (time(NULL));

  measure(test_copy_return, "Copy return");
  measure(test_move_return, "Move return");
  measure(test_copy_swap,   "Copy swap");
  measure(test_move_swap,   "Move swap");

  return 0;
}

// > g++ move_rvo_test.cpp -std=c++11 -o prog.exe
// > prog.exe
// Copy return: 224ms
// Move return: 25ms
// Copy swap: 524ms
// Move swap: 2ms

// > g++ move_rvo_test.cpp -std=c++11 -o prog.exe -fno-elide-constructors
// > prog.exe
// Copy return: 500ms
// Move return: 33ms
// Copy swap: 522ms
// Move swap: 1ms

Advanced C++: Constructors and destructors

By Cserép Máté

Advanced C++: Constructors and destructors

  • 214