C++ Core Guidelines: Regeln für Klassen

Modernes C++  –  9 Kommentare

Eine Klasse ist ein benutzerdefinierter Typ, für die Programmierer die Repräsentation, die Operationen und das Interface festlegen. Die C++ Core Guidelines besitzen sehr viele Regeln für benutzerdefinierte Typen.

Die Guidelines beginnen mit allgemeinen Regeln für Klassen, besitzen aber auch spezielle Regeln für Konstruktoren und Destruktoren, zu Klassenhierarchien sowie zum Überladen von Operatoren und Unions.

Bevor ich mich ausführlich den spannenden, speziellen Regeln widme, gehe ich erst auf die acht allgemeinen Regeln ein.

  • C.1: Organize related data into structures (structs or classes)
  • C.2: Use class if the class has an invariant; use struct if the data members can vary independently
  • C.3: Represent the distinction between an interface and an implementation using a class
  • C.4: Make a function a member only if it needs direct access to the representation of a class
  • C.5: Place helper functions in the same namespace as the class they support
  • C.7: Don’t define a class or enum and declare a variable of its type in the same statement
  • C.8: Use class rather than struct if any member is non-public
  • C.9: Minimize exposure of members

Ich werde nur soweit auf die allgemeinen Regeln zu Klassen eingehen, damit ihre Intention ersichtlich wird.

Allgemeine Regeln zu Klassen


C.1: Organize related data into structures (structs or classes)

Wenn Daten zusammen gehören, sollte man sie in einer Klasse oder Struktur kapseln. Daher ist die zweite Funktion deutlich einfacher zu verstehen.

void draw(int x, int y, int x2, int y2);  // BAD: unnecessary implicit relationships
void draw(Point from, Point to); // better

C.2: Use class if the class has an invariant; use struct if the data members can vary independently

Eine Invariante ist eine logische Bedingung, die typischerweise im Konstruktor etabliert wird.

struct Pair {  // the members can vary independently
string name;
int volume;
};

class Date {
public:
// validate that {yy, mm, dd} is a valid date and initialize
Date(int yy, Month mm, char dd);
// ...
private:
int y;
Month m;
char d; // day
};

Die Klasse Date besitzt die Invariante y, m und d. Diese wird im Konstruktor initialisiert und geprüft. Der Datentyp Pair besitzt hingegen keine Invariante. Daher kommt eine Struktur zum Einsatz.

Dank der Invariante ist die Klasse einfacher zu verwenden. Einfachheit ist auch der Grund für die nächste Regel.

C.3: Represent the distinction between an interface and an implementation using a class

Die öffentlichen Methoden stellen in diesem Fall das Interface der Klasse dar, die privaten Methoden deren Implementierung.

class Date {
// ... some representation ...
public:
Date();
// validate that {yy, mm, dd} is a valid date and initialize
Date(int yy, Month mm, char dd);

int day() const;
Month month() const;
// ...
};

Mit der Mainainance-Brille betrachtet lässt sich die Implementierung der Klasse Date ändern, ohne dass die Anwender betroffen sind.

C.4: Make a function a member only if it needs direct access to the representation of a class

Falls eine Funktion nicht den Zugriff auf die Implementierung einer Klasse benötigt, sollte sie kein Mitglied der Klasse sein. Damit erhält man automatisch lose Kopplung, und eine Veränderung der Implementierung der Klasse beeinträchtigt nicht die Funktion.

C.5: Place helper functions in the same namespace as the class they support

Hilfsfunktion sollten im gleichen Namensraum wie die Klasse sein.

namespace Chrono { // here we keep time-related services

class Date { /* ... */ };

// helper functions:
bool operator==(Date, Date);
Date next_weekday(Date);
// ...
}
...
if (date1 == date2){ ... // (1)

Dank Argument Dependent Lookup (ADL) findet der C++ Lookup den Identitätsoperator (==) im Chrono-Namensraum.

C.7: Don’t define a class or enum and declare a variable of its type in the same statement

Ich gebe zu: Die Definition einer Klasse und die Deklaration einer Variable dieses Typs hat mich schon das eine oder andere Mal verwirrt.

// bad
struct Data { /*...*/ } data{ /*...*/ };

// good
struct Data { /*...*/ };
Data data{ /*...*/ };

C.8: Use class rather than struct if any member is non-public

Dies ist eine praktische und bereits sehr häufig eingesetzte Regel. Falls ein Datentyp "private"- oder "protected"-Mitglieder besitzt, sollte er als Klasse entworfen werden.

C.9: Minimize exposure of members

Die Regel ist auch unter dem Ausdruck Data Hiding bekannt und ist einer der Eckpfeiler objektorientierten Klassendesigns. Sie besagt, dass man sich Gedanken zu zwei Arten von Interfaces machen sollte: ein öffentliches Interface für den allgemeinen Anwendungsfall und ein "protected"-Interface für die abgeleiteten Klassen. Die verbleibenden Mitglieder sollten privat sein.

Nun kommen wir zu den spezielleren Regeln. Hier ist der erste Überblick.

  • C.concrete: Concrete types
  • C.ctor: Constructors, assignments, and destructors
  • C.con: Containers and other resource handles
  • C.lambdas: Function objects and lambdas
  • C.hier: Class hierarchies (OOP)
  • C.over: Overloading and overloaded operators
  • C.union: Unions

Weiter geht's mit den zwei Regeln zu Concrete Types.

Concrete Types

  • C.10: Prefer concrete types over class hierarchies
  • C.11: Make concrete types regular

Zuerst möchte ich auf Concrete Types und Regular Types eingehen.

Ein Concrete Type ist "the simplest kind of a class". Gerne wird er auch Value Type genannt. Er ist ein Bestandteil einer Klassenhierarchie. Natürlich kann ein Concrete Type nicht abstrakt sein.

Ein Regular Type ist ein Datentyp, für den gilt: "behaves like an int". Daher muss er Kopieren und Zuweisen, aber auch Gleichheit und Ordnung unterstützen. Das geht auch formaler. Ein Regular Type muss die folgenden Operation anbieten.

  • Copy und Zuweisung
    Regular a;
Regular a = b;
~Regular(a);
a = b;
  • Gleichheit
    a == b;
a != b;
  • Ordnung
    a < b;

Die Built-in-Datentypen sind genauso Regular Types wie die Container der Standard Template Library.

C.10: Prefer concrete types over class hierarchies

Falls man keinen Anwendungsfall für eine Klassenhierarchie hat, verwendet man einen Concrete Type. Er ist einfacher zu implementieren, kleiner und schneller. Man muss sich keine Gedanken zur Ableitung machen, Virtualität, Referenzen oder Zeiger. Diese Vereinfachung schließt Speicheranforderungen und -freigaben ein. Es gibt keinen virtuellen Dispatch und daher keine unnötige Indirektion zur Laufzeit.

Man besitzt einfach nur einen Wert.

C.11: Make concrete types regular

Regular Types (ints) sind einfacher zu verstehen. Sie sind per se intuitiv. Das heißt, falls man einen Concrete Type verwendest, muss man darüber nachdenken, ihn zum Regular Type zu erweitern.

Wie geht's weiter?

Im nächsten Artikel geht es um den Lebenszyklus von Objekten: erzeugen, kopieren, verschieben und löschen.