stork 1324371 1280

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. Why not just ten rules? Because the eleventh rule is just too attractive. 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;
};

 

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

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

    This rule is quite similar to the previous one. Accordingly, creating the fully initialized object is the job of the constructor. A class having an 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 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 must check its state before its usage. This is highly 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 STL containers rely 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 class members 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 favorite features of C++11. Initializing class members directly in the class body makes the writing of constructors a lot easier and sometimes obsolete. Class X1 classically defines its members (before C++11) and X2 in a preferred way. A nice side effect is that the compiler automatically generates 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 crucial rule. Single-argument constructors are often called conversion constructors. 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 

     

    The implicit conversion from int to String is impossible because the constructor is explicit. If instead of the explicit constructor, the out-commented implicit constructor were 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 initialized 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 initializer makes it a lot easier to define the constructors. Additionally, you can not forget to initialize 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 initialization establishes the default behavior of an object, the constructor (1) allows the variation of the default behavior.

    C.49: Prefer initialization to assignment in constructors

    That is quite an old rule. The most obvious pros of initialization to the assignment are that you can not forget to assign a value and use it uninitialized. Initialization 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 as expected. For protection reasons, the virtual call mechanism is disabled in the constructor because the creation of the derived class hasn’t happened.

    Hence, the following example will call the Base version of the virtual function f.

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

     

     

     

    Let’s create a factory function to have virtual behavior during object initialization. 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 initialization, 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 behavior during initialization.

    virtualInitialisation

    There is one risk with this technique. If the base goes out of scope, you must ensure that the Derived destructor is called. This is the reason for the virtual destructor of Base (4).  If the destructor is not virtual, you will get undefined behavior. 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 unnecessary. 

    What’s next?

    Sorry, the post is a little bit too long. But I found, in particular, 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 Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *