Modern C++ Workshop

Day 2 - Classes

Agenda

constructor delegation

rule of five

default keyword

delete keyword

final and override

initializer lists

uniform initialization

non-static data member initialization

construction/destruction order

Constructor Delegation

class SomeType {
    int         number;
    std::string name;

public:
    SomeType(int number, std::string name)
      : number(number)
      , name(name)
    {}

    SomeType(int number)
      : SomeType(number, "foo")
    {}

    SomeType(std::string name)
      : SomeType(42, name)
    {}

    SomeType()
      : SomeType(42, "foo")
    {}
};

Non-modern C++ did not allow constructors to call each other.

 

Starting with C++11, constructors can delegate calls to other constructors inside the same class.

 

In a corresponding exercise, you'll use this to refactor code.

C++98 revisit:

special methods

#include <iostream>

struct A {
    // constructor
    A()
      : name("hello")
      {
         std::cout << "constructor";
      }
     
    // destructor
    ~A() {
      std::cout << "destructor";
    }

    // copy constructor
    A(const A& other)
      : name(other.name)
    {
       std::cout << "copy constructor";
    }
    // copy assignment operator
    A& operator= (const A& other) {
      name = other.name;
      std::cout << "copy assignment";
      return *this;
    }
    std::string name;    
};

int main(int argc, char** argv) {
  A var1;
  A var2 = var1;
  var1 = var2;
}

C++98 revisit:

special methods

#include <iostream>

struct A {
    // constructor
    A()
      : name("hello")
      {
         std::cout << "constructor";
      }
     
    // destructor
    ~A() {
      std::cout << "destructor";
    }

    // copy constructor
    A(const A& other)
      : name(other.name)
    {
       std::cout << "copy constructor";
    }
    // copy assignment operator
    A& operator= (const A& other) {
      name = other.name;
      std::cout << "copy assignment";
      return *this;
    }
    std::string name;    
};

int main(int argc, char** argv) {
  A var1;
  A var2 = var1;
  var1 = var2;
}
// output:
// constructor
// copy constructor
// copy assignment
// destructor
// destructor

Compiler generated methods

Default constructors initialize each member in order of declaration using their default constructor.

 

Default destructors destruct members in reverse order of their declaration.

 

Default copy constructors, copy assignment operators, move constructors and move assignment operators perform member-wise copy/move, respectively.

pre C++11: Rule of Three

If you define one of the following, you probably need to define all three:

  • destructor
  • copy constructor
  • copy assignment operator

C++11: Rule of Five

If you define one of the following, you probably need to define all three:

  • destructor
  • copy constructor
  • copy assignment operator
  • move constructor
  • move assignment operator

Default Keyword

struct Interface {
   // compiler generated
   // virtual destructor
   virtual ~Interface() = default; 
   virtual void method() = 0;
};

class POD {
  public:
    // compiler generated default constructor
    // this is needed because 
    // defining another constructor
    // causes this one not to be generated
    POD() = default;
    POD(const std::string& name)
       : name(name)
    {}
  private:
    std::string name;
}
    

Default-generated methods are guaranteed to be inlineable by compilers (performance gains).

Delete Keyword

// stolen from boost
struct noncopyable {
    noncopyable() = default;
    ~noncopyable() = default;
    noncopyable(
      const noncopyable&) = delete;
    noncopyable& operator=(
      const noncopyable&) = delete;
}
  

Inheritance vs Composition

"Program to an 'interface', not an 'implementation'."

(page 18)


Composition over inheritance: "Favor 'object composition' over 'class inheritance'."

(page 20)

Final Keyword

struct Base
{
    virtual void foo();
};
 
struct A : Base
{
    // A::foo is final
    void foo() final; 
    // Error: non-virtual function cannot be final
    void bar() final;
};
 
// struct B is final
struct B final : A
{
    // Error: foo cannot be overridden as it's final in A
    void foo();
    
};
 
// Error: B is final
struct C : B
{
};

1. used in classes to prevent inheritance

 

2. used in methods to prevent overloading by derived classes

Override Keyword

struct A
{
    virtual void foo();
    void bar();
};
 
struct B : A
{
    // Error: B::foo does not override A::foo
    void foo() const final override; 
                        
    // OK: B::foo overrides A::foo       
    void foo() final override;

    // Error: A::bar is not virtual
    void bar() override;
};

override is used to statically ensure that a method is really overriding a base classes virtual method

std::initializer_list

std::vector<int> vec = {1, 2, -4, 12};

for(const auto i : {1, 6, 12, -8, 15}) {
  std::cout << i << std::endl;
}

template <typename T>
class MyContainer {
    MyContainer(std::initializer_list<T> t) {
       for(const auto& element : t) {
          // do something with element
       }
    }
}

initialization

widget w;                   // (a)

widget w();                 // (b)
widget w{};                 // (c)

widget w(x);                // (d)
widget w{x};                // (e)

widget w = x;               // (f)
widget w = {x};             // (g)

auto w = x;                 // (h)
auto w = widget{x};         // (i)

What is the difference between those?

initialization

widget w;                   // (a)

w is constructed using the default constructor of class widget

 

or

 

w is uninitialized iff widget is a built in type

initialization

widget w();                 // (b)
widget w{};                 // (c)

(b) is actually a function declaration

 

(c) was introduced in C++11 and guarantees the default constructor is called

(even if there is a constructor taking std::initializer_list as an argument)

initialization

widget w(x);                // (d)
widget w{x};                // (e)

(d) and (e) initialize widget by calling its constructor, passing x

 

If x is a type, (d) is actually a function declaration, even if there is a variable x in scope

(e) is never a function declaration

initialization

widget w(x);                // (d)
widget w{x};                // (e)

(e) avoids narrowing:

struct A {
  A(int n) : number(n) {}
  int number;
}

// ok, implicit conversion
A one(1.23);

// compiler error, narrowing
A two{1.23};

initialization

widget w = x;               // (f)
widget w = {x};             // (g)

(f) is copy initialization, (g) is copy list initialization no assignment operator is involved

The same reasons apply to prefer (g) over (f) as with (e) over (d)

If x is not of type widget, implicit conversions can occur with (f), but not with (g).

initialization

auto w = x;                 // (h)
auto w = widget{x};         // (i)

semantics are the same as with (f) and (g), but there are less ways to shoot yourself in the foot

(h) never converts types, guaranteed to only call a single copy constructor

(i) allows you to commit to a type, but avoid narrowing conversions

initialization

widget w;                   // (a)

widget w();                 // (b)
// prefer this
widget w{};                 // (c)

widget w(x);                // (d)
widget w{x};                // (e)

widget w = x;               // (f)
widget w = {x};             // (g)

// prefer these
auto w = x;                 // (h)
auto w = widget{x};         // (i)

non-static data member initialization

struct A {
  A() = default;
  A(const std::string& name)
    : name(name)
  {}

  std::string name = "42";
}

A one;
auto two = A{"hello"};

std::cout << one.name << std::endl;
std::cout << two.name << std::endl;

construction order

  1. all virtual base classes in type hierarchy depth-first, left-to-right
  2. constructors execute non-virtual base class constructors first, left to right order
  3. members are initialized in declaration order, initializer list or non-static member defaults, whichever applicable
  4. the constructor body is executed

construction order

struct A1{};
struct A2{};
struct A : A2, A1{};
struct B {};
struct D : A, B {
  D()
    : name("foo")
    , a({13}) 
  {
    a = {11};
    name = "bar";
  }
  std::string name
    = "abc";
  std::vector<int> a
    = {3};
};

Whiteboard exercise: name all called constructors and their arguments in the right order.

  1. all virtual base classes in type hierarchy depth-first, left-to-right
  2. constructors execute non-virtual base class constructors first, left to right order
  3. members are initialized in declaration order, initializer list or non-static member defaults, whichever applicable
  4. the constructor body is executed

Thanks,

Questions?

Modern C++ Workshop - Classes

By Jupp Müller

Modern C++ Workshop - Classes

  • 866