C++ Core Guidelines: Constructors

The lifecycle of each object starts with its creation; therefore, this post will be about the thirteen most fundamental rules for objects: constructor rules.

 

Twelve rules are too many for one post. Therefore, I will cover only the first eleven ones. Why not just ten rules? Because the eleventh rule is just too interesting. The remaining two are part of the next post. Here are the thirteen rules.

stork 1324371 1280

Constructor rules:

 

So, let's look at the rules in detail. For further analysis, use the links to the rules.

C.40: Define a constructor if a class has an invariant

An invariant of an object is a characteristic of the object that should hold for its entire lifetime. The place to establish such an invariant is the constructor. An invariant can be a valid date.

class Date {  // a Date represents a valid date
              // in the January 1, 1900 to December 31, 2100 range
    Date(int dd, int mm, int yy)
        :d{dd}, m{mm}, y{yy}
    {
        if (!is_valid(d, m, y)) throw Bad_date{};  // enforce invariant
    }
    // ...
private:
    int d, m, y;
};

 

C.41: A constructor should create a fully initialized object

This rule is quite similar to the previous one. Accordingly, create the fully initialized object is the job of the constructor. A class having a init method is asking for trouble.

class X1 {
    FILE* f;   // call init() before any other function
    // ...
public:
    X1() {}
    void init();   // initialize f
    void read();   // read from f
    // ...
};

void f()
{
    X1 file;
    file.read();   // crash or bad read!
    // ...
    file.init();   // too late
    // ...
}

 

The user might mistakenly invoke read before init or might just forget to invoke init.

C.42: If a constructor cannot construct a valid object, throw an exception

Accordingly to the previous rule: throw an exception if you can not construct a valid object. There is not much to add. If you work with an invalid object you have always to check the state of the object before its usage. This is extremely error-prone. Here is an example from the guidelines:

class X3 {     // bad: the constructor leaves a non-valid object behind
    FILE* f;  
    bool valid;
    // ...
public:
    X3(const string& name)
        :f{fopen(name.c_str(), "r")}, valid{false}
    {
        if (f) valid = true;
        // ...
    }

    bool is_valid() { return valid; }
    void read();   // read from f
    // ...
};

void f()
{
    X3 file {"Heraclides"};
    file.read();   // crash or bad read!
    // ...
    if (file.is_valid()) {
        file.read();
        // ...
    }
    else {
        // ... handle error ...
    }
    // ...
}

 

C.43: Ensure that a value type class has a default constructor

A value type is a type that behaves like an int. A value type is similar to a regular type. I wrote about value types and regular types in the post about concrete types. Having a default constructor makes it easier to use your type. Many constructors of STLcontainer relies on the fact that your type has a default constructor. For example for the value of an ordered associative container such as std::map. If all the members of the class have a default constructor, the compiler will implicitly generate one for your class.

C.44: Prefer default constructors to be simple and non-throwing

Error handling is a lot easier with default constructors that can not throw. The guidelines provide a simple example:

 

template<typename T>
// elem is nullptr or elem points to space-elem element allocated using new
class Vector1 {
public:
    // sets the representation to {nullptr, nullptr, nullptr}; doesn't throw
    Vector1() noexcept {}
    Vector1(int n) :elem{new T[n]}, space{elem + n}, last{elem} {}
    // ...
private:
    own<T*> elem = nullptr;
    T* space = nullptr;
    T* last = nullptr;
};

 

C.45: Don’t define a default constructor that only initializes data members; use member initializers instead

This is one of my favourite features from C++11. Defining class members directly in the class body make the writing of constructors a lot easier and sometimes obsolete. The class X1 defines its members in a classical way (before C++11) and X2 in the preferred way. A nice side effect is that the compiler will automatically generate the constructor for X2.

class X1 { // BAD: doesn't use member initializers
    string s;
    int i;
public:
    X1() :s{"default"}, i{1} { }
    // ...
};

class X2 {
    string s = "default";
    int i = 1;
public:
    // use compiler-generated default constructor
    // ...
};

 

C.46: By default, declare single-argument constructors explicit

This is a very important rule. Single-argument constructors are often called conversion constructor. If you make them not explicit an implicit conversion may happen.

class String {
public:
    explicit String(int);  // explicit
    // String(int);        // implicit
};

String s = 10;            // error because of explicit 

 

Using the implicit conversion from int to String is not possible because the constructor is explicit. If instead of the explicit constructor the out-commented implicit constructor would be used, you would get a String of size 10

C.47: Define and initialize member variables in the order of member declaration

The class members are initialised in the order of their declaration. If you initialize them in the constructor initializer in a different order you may get surprised.

class Foo {
    int m1;
    int m2;
public:
    Foo(int x) :m2{x}, m1{++x} { }   // BAD: misleading initializer order
    // ...
};

Foo x(1); // surprise: x.m1 == x.m2 == 2

 

C.48: Prefer in-class initializers to member initializers in constructors for constant initializers

In-class initialiser makes it a lot easier to define the constructors. Additionally, you can not forget to initialise a member.

 

class X {   // BAD
    int i;
    string s;
    int j;
public:
    X() :i{666}, s{"qqq"} { }   // j is uninitialized
    X(int ii) :i{ii} {}         // s is "" and j is uninitialized
    // ...
};

class X2 {
    int i {666};
    string s {"qqq"};
    int j {0};
public:
    X2() = default;        // all members are initialized to their defaults
    X2(int ii) :i{ii} {}   // s and j initialized to their defaults  (1)
    // ...
};

 

While the in-class initialisation establishes the default behaviour of an object, the constructor (1) allows the variation of the default behaviour.

C.49: Prefer initialization to assignment in constructors

That is a quite old rule. The most obvious pros of initialisation to the assignment are: you can not forget to assign a value and use it uninitialised and initialisation may be faster but never slower than assignment.

class B {   // BAD
    string s1;
public:
    B() { s1 = "Hello, "; }   // BAD: default constructor followed by assignment
    // ...
};

 

C.50: Use a factory function if you need “virtual behavior” during initialization

Calling a virtual function from a constructor will not work the way you may expect. For protection reasons, the virtual call mechanism is disabled in the constructor because the creation of the derived class hasn't happened.

Hence, the Base version of the virtual function f will be called in the following example.

// virtualConstructor.cpp

#include <iostream>

struct Base{
  Base(){
    f();
  }
  virtual void f(){
    std::cout << "Base called" << std::endl;
  }
};

struct Derived: Base{
  virtual void f(){
    std::cout << "Derived called" << std::endl;
  }
};

int main(){
  
  std::cout << std::endl;
  
  Derived d;         
  
  std::cout << std::endl;
  
};

 

 Here is the output of the program.

virtualConstructor

 

 

 

Now, let's create a factory function to have virtual behaviour during object initialisation. To deal with the ownership, the factory function should return a smart pointer such as a std::unique_ptr or a std::shared_ptr. As a starting point, I will use the previous example but make the constructor of Base protected; therefore, only objects of the class Derived can be created.

// virtualInitialisation.cpp

#include <iostream>
#include <memory>

class Base{
protected:
  Base() = default;
public:
  virtual void f(){                                            // (1)
    std::cout << "Base called" << std::endl;                   
  }
  template<class T>                                              
  static std::unique_ptr<T> CreateMe(){                        // (2) 
    auto uniq = std::make_unique<T>();
    uniq->f();                                                 // (3)
    return uniq;
  }
  virtual ~Base() = default;                                   // (4)
};

struct Derived: Base{
  virtual void f(){
    std::cout << "Derived called" << std::endl;
  }
};


int main(){
  
  std::cout << std::endl;
  
  std::unique_ptr<Base> base = Derived::CreateMe<Derived>();   // (5)
  
  std::cout << std::endl;
  
};

 

At the end of the initialisation, the virtual function f (1) should be called. (2) is the factory function. This factory function calls f after creating a std::unique_ptr and returns it. If Derived is derived from Base, then std::unique_ptr<Dervived> is implicitly convertible to a std::unique_ptr<Base>. Finally, we get our virtual behaviour during initialisation.

virtualInitialisation

There is one risk with this technique. If base goes out of scope you have to ensure that the destructor of Derived is called. This is the reason for the virtual destructor of Base (4).  If the destructor is not virtual, you will get undefined behaviour. Strange but if I used a std::shared_ptr instead of a std::unique_ptr for the factory method, the virtual destructor of Base is not necessary. 

What's next?

Sorry, the post is a little bit too long. But I found in particularl the last rule (C.50) very interesting; therefore, I had to explain more than usual. In the next post, I will finish the rules for constructors and start with the copy and move rules.

 

 

Thanks a lot to my Patreon Supporter: Eric Pederson.

 

Get your e-book at leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11 and C++14 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 500 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Comments   

0 #1 Mikhail Virovets 2017-09-11 19:33
It seems that C.50 contradicts C.41. Could this be resolved by saying 'Any public constructor...' in C.41?
Quote
+1 #2 Paul Varghese 2017-09-11 22:08
in section C.50, better to use override keyword instead of virtual for overriding the factory method.
Quote
0 #3 Rainer Grimm 2017-09-12 05:25
Quoting Mikhail Virovets:
It seems that C.50 contradicts C.41. Could this be resolved by saying 'Any public constructor...' in C.41?

You are right. I stick to my old habits.
Quote

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 376

All 496949

Currently are 157 guests and no members online