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.
Constructor rules:
- C.40: Define a constructor if a class has an invariant
- C.41: A constructor should create a fully initialized object
- C.42: If a constructor cannot construct a valid object, throw an exception
- C.43: Ensure that a value type class has a default constructor
- C.44: Prefer default constructors to be simple and non-throwing
- C.45: Don’t define a default constructor that only initializes data members; use member initializers instead
- C.46: By default, declare single-argument constructors
explicit
- C.47: Define and initialize member variables in the order of member declaration
- C.48: Prefer in-class initializers to member initializers in constructors for constant initializers
- C.49: Prefer initialization to assignment in constructors
- C.50: Use a factory function if you need “virtual behavior” during initialization
- C.51: Use delegating constructors to represent common actions for all constructors of a class
- C.52: Use inheriting constructors to import constructors into a derived class that does not need further explicit initialization
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; };
Modernes C++ Mentoring
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.
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.
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 |
Seminars
I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.
Standard Seminars (English/German)
Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.
- C++ – The Core Language
- C++ – The Standard Library
- C++ – Compact
- C++11 and C++14
- Concurrency with Modern C++
- Design Pattern and Architectural Pattern with C++
- Embedded Programming with Modern C++
- Generic Programming (Templates) with C++
- Clean Code with Modern C++
- C++20
Contact Me
- Mobil: +49 176 5506 5086
- Mail: schulung@ModernesCpp.de
- German Seminar Page: www.ModernesCpp.de
- Mentoring Page: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!