C++ Core Guidelines: Regeln für Konstruktoren
Der Lebenszyklus jedes Objekts beginnt mit seiner Erzeugung. Somit beschäftigt sich dieser Artikel mit den dreizehn fundamentalsten Regeln für Objekte: denen für Konstruktoren.
- Rainer Grimm
Der Lebenszyklus jedes Objekts beginnt mit seiner Erzeugung. Somit beschäftigt sich dieser Artikel mit den dreizehn fundamentalsten Regeln für Objekte: denen für Konstruktoren.
Dreizehn Regeln sind eindeutig zu viel für einen Artikel. In diesem Artikel geht es nur um die ersten elf Regeln. Warum nicht nur zehn Regeln? Die elfte Regel ist einfach zu interessant. Die verbleibenden zwei Regeln spare ich mir für den nächsten Artikel auf. Hier sind alle Regeln kurz und bündig.
Regeln für Konstruktoren:
- 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
Auf die Suche in Breite folgt in bekannter Manier die Suche in die Tiefe. Weitere Details lassen sich einfach mit den Links auf die Regeln nachlesen.
C.40: Define a constructor if a class has an invariant
Eine Invariante ist eine Charakteristik eines Objekts, die für seinen ganzen Lebenszyklus gelten soll. Der Platz, um eine Invariante zu etablieren, ist der Konstruktor. Eine Invariante kann ein gültiges Datum sein.
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
Diese Regel schlägt eine ähnlichen Ton an wie ihr Vorgänger. Es gilt, dass es die Aufgabe eines Konstruktors ist, ein fertig initialisiertes Objekt zu erzeugen. Besitzt eine Klasse eine init Methode, gehen die Probleme typischerweise bereits los.
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
// ...
}
Der Anwender kann irrtümlicherweise read befor init aufrufen oder schlicht den Aufruf von init vergessen.
C.42: If a constructor cannot construct a valid object, throw an exception
Entsprechend zur vorherigen Regel gilt: Wirf eine Ausnahme, falls du kein gültiges Objekt erzeugen kannst. Da gibt es nicht viel hinzuzufügen. Falls ein ungültiges Objekt verwendet wird, muss vor jeder Verwendung des Objekts seinen Zustand geprüft werden. Wenn das nicht fehleranfällig ist? Hier ist ein Beispiel aus den 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
Ein Value Type ist ein Datentyp, der sich wie ein int verhält. Ein Value Type ist einem Regular Type sehr ähnlich. Ich habe in dem Artikel zu Concrete Types über Value Types und Regular Types geschrieben. Falls ein Datentyp einen Default-Konstruktor besitzt, lässt es sich deutlich einfacher mit ihm arbeiten. Viele Konstruktoren der STL-Container verlassen sich darauf, dass ein Datentyp einen Default-Konstruktor besitzt. Zum Beispiel der Wert eines geordneten assoziativen Containers wie std::map. Falls alle Mitglieder einer Klasse einen Default-Konstruktor besitzen, erzeugt der Compiler automatisch einen Default-Konstruktor für diese Klasse.
C.44: Prefer default constructors to be simple and non-throwing
Fehlerbehandlung ist einfacher mit Default-Konstruktoren, die keine Ausnahme werfen können. Die Guidelines bieten ein einfaches Beispiel an.
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
Dies ist eines meiner Lieblingsfeatures aus C++11. Wenn du Klassenmitglieder direkt im Klassenkörper initialisierst, wird das Schreiben von Konstruktoren deutlich einfacher und manchmal sogar überflüssig. Die Klasse X1 definiert seine Mitglieder in der klassischen Weise (bevor C++11) und die Klasse X2 in der vorzuziehenden Weise. Ein schöner Seiteneffekt der Klasse X2 ist es, dass der Compiler automatisch den Konstruktor für die Klasse X2 erzeugt.
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
Die Anwendung dieser Regel ist sehr wichtig und schützt vor bösen Überraschungen. Konstruktoren, die nur ein Argument annehmen, werden gerne auch Konvertierungs-Konstruktor genannt, da sie das Argument in eine Instanz der Klasse konvertieren. Falls solch einen Konvertierungs-Konstruktor nicht als explizit deklariert ist, lauert immer der Gefahr der impliziten Typkonvertierung. Das Codeschnipsel macht es richtig.
class String {
public:
explicit String(int); // explicit
// String(int); // implicit
};
String s = 10; // error because of explicit
Die implizite Konvertierung von int nach String ist in diesem Beipiel nicht möglich, da der Konstruktor als explizit deklariert wurde. Falls statt des expliziten Konstruktors der auskommentierte, implizite Konstruktor zum Einsatz käme, würde die letzte Zeile einen String der Länge 10 erzeugen.
C.47: Define and initialize member variables in the order of member declaration
Klassenmitglieder werden in der Reihenfolge ihre Deklaration initialisiert. Falls sie in einer anderen Reihenfolge in dem Konstruktor-Initialisierer initialisiert werden, mag das Verhalten den einen oder anderen überraschen.
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
Wenn Klassenmitglieder direkt im Klassenkörper initialisiert werden, wird das Schreiben von Konstruktoren deutlich einfacher. Zusätzlich kannst du auf die Art nicht vergessen, ein Klassenmitglied zu initialisieren.
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)
// ...
};
Während die Initialisierung von Klassenmitglieder im Klassenkörper das Default-Verhalten für die Objekt einer Klasse etabliert, erlaubt der Konstruktor (1) diese Default-Verhalten zu variieren.
C.49: Prefer initialization to assignment in constructors
Diese Regel ist schon lang in der Anwendung. Die offensichtlichsten Gründe für Initialisierung gegenüber Zuweisung sind: Du kannst nicht vergessen, einen Wert zu initialisieren und ihn daher uninitialisiert verwenden und die Initialisierung ist meistens schneller, aber nicht langsamer als die Zuweisung.
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
Der Aufruf einer virtuellen Funktion aus dem Konstruktor verhält sich besonders. Um den Anwender zu schützen, wird der virtuelle Aufruf im Konstruktor unterbunden, da die abgeleiteten Klassen zu diesem Zeitpunkt noch nicht erzeugt sind.
Daher wird in diesem Beispiel die Base-Variante der virtuellen Funktion f aufgerufen.
// 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;
};
Hier ist die Ausgabe des Programms:
Jetzt werde ich eine Fabrikmethode implementieren, um virtuelles Verhalten während der Objektinitialisierung zu erhalten. Um mit den Besitzverhältnissen richtig umzugehen, sollte die Fabrikmethode einen Smart Pointer wie std::unique_ptr oder std::shared_ptr zurückgeben. Als Startpunkt meiner Implementierung kommt das vorherige Beispiel zum Einsatz. Damit nur Objekte vom Typ Derived erzeugt werden können, setze ich den Konstruktor von Base auf protected.
// 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;
};
Als letzter Schritt der Initialisierung soll die virtuelle Funktion f (1) aufgerufen werden. (2) ist die Fabrikmethode. Die Fabrikmethode ruft f auf, nachdem sie einen std::unqiue_ptr erzeugt hat und gibt diesen zurück. Wenn Derived von Base abgeleitet ist, dann ist std::unique_ptr<Derived> implizit nach std::unique_ptr<Base> konvertierbar. So erhalten wir virtuelles Verhalten während der Initialiserung.
Es gibt eine Gefahr beim Einsatz dieser Technik. Falls base seine Gültigkeit verliert, muss sichergestellt sein, dass der Destruktor von Derived aufgerufen wird. Das ist der Grund für den virtuellen Destruktor von Base (4). Wäre der Destruktor nicht virtuell, würde undefiniertes Verhalten resultieren. Seltsam: Wenn ich einen std::shared_ptr anstelle eines std::unique_ptr in der Fabrikmethode verwendet hätte, wäre der virtuelle Destruktor von Base nicht notwendig gewesen.
Wie geht's weiter?
Sorry, aber der Artikel wurde ein wenig länglich. Aber ich fand gerade die letzte Regel (C.50) sehr interessant. Daher habe ich mehr dazu geschrieben als üblich. Im nächsten Artikel werde ich die Regeln für Konstruktoren abschließen und dann gibt es schon die Regeln für das Kopieren und Verschieben von Objekten.
Weitere Informationen
- Source Code: Den Source Code zu den ausführbaren Dateien gibt es auf meinem GitHub-Account: ModernesCppSource
- Aktuelles pdf-Päckchen: Alle 4-6 Wochen veröffentliche ich nach einer Abstimmung ein Päckchen zu meinen bisherigen Artikeln. Diese Päckchen enthält alle Artikel, den Source Code und eine minimale cmake-Datei zu dem gewünschten Thema. Wie der Download funktioniert, habe ich im Artikel "Das neue pdf-Päckchen ist fertig: Embedded: Hohe Sicherheitsanforderungen" beschrieben.