C++11 Defaulted & Deleted Functions
Mit der Version C++11 finden einige größere und viele kleinere Verbesserungen Einzug in die Sprache C++. In diesem Artikel werfe ich einen genaueren Blick auf die Möglichkeiten der sogenannten defaulted
und deleted
Funktionen.
Defaulted Special Functions
In Objekt-orientierten Programmiersprachen ist es üblich, dass der Compiler bestimmte Funktionen automatisch erzeugt, wenn der Benutzer nicht selbst entsprechende Funktionen definiert. Während in Java das nur für den Default-Konstruktor geschieht, der erzeugt wird falls der Programmierer keinen Konstruktor definiert, gibt es in C++ bereits eine Reihe dieser speziellen Funktionen. Neben dem Default-Konstruktor gehören der Destruktor, der Copy-Konstruktor sowie der Zuweisungsoperator dazu. C++11 erweitert die Liste noch um die entsprechenden Funktionen mit Move-Semantik.
Die Regeln für die automatische Erzeugung sind allerdings nicht trivial. Um den Entwickler mehr Kontrolle darüber zu geben, bietet C++11 nun die Möglichkeit, den Compiler explizit anzuweisen, bestimmte Default-Funktionen selbst zu implementieren. Dafür wird die Syntax =default
verwendet, um am einfachsten kann ich sie am Default-Konstruktor zeigen.
class foobar
{
std::string _name;
public:
/* Because a constructor has been declared, the compiler
* does not generate the default constructor ... */
explicit foobar(std::string name);
/* NOTE: ... unless it is explicitly told to do so. */
foobar() = default;
};
Der Default-Konstruktor ist in der Regel sehr einfach selbst zu implementieren. Die gleiche Syntax funktioniert aber auch für die anderen speziellen Funktionen, die oben bereits erwähnt wurden, und spätestens bei den Copy- und Move-Konstruktoren macht sich der Vorteil bemerkbar. Darüber hinaus ist für zukünftige Erweiterungen vorgesehen, mit diesem Mechanismus auch ganz andere Funktionen zu erzeugen. Beispielsweise könnte der Compiler die Vergleichsoperatoren und swap-Funktionen für einfache Klassen selbst generieren, wenn man ihn explizit dazu anweist.
Deleted Special Functions
In gewisser Weise sind die deleted
Funktionen das Gegenteil davon. Mit ihnen kann sichergestellt werden, dass bestimmte Funkionen nicht aufgerufen werden. Die naheliegendste Anwendung dafür sind nicht kopierbare Klassen, für die bisher bestimmte Funktionen privat deklariert wurden.
class foobar
{
void* _handle; // directly managed pointer
public:
foobar();
~foobar();
// NOTE: disable copy constructor and assignment operator
foobar(const foobar&) = delete;
foobar& operator=(const foobar&) & = delete;
// ... further members
};
Dieser Fall wurde bisher in der Regel mit Hilfe privater Funktionen adressiert. Mit der neuen Möglichkeit wird die Absicht des Entwicklers aber klarer, die Fehlermeldungen des Compilers besser und es werden auch Randfälle ausgeschlossen, wie beispielsweise der Versuch die Kopier-Operation innerhalb einer Member-Funktion durchzuführen.
Deleted Functions im Allgemeinen
Aus dem gerade geschriebenen könnte man annehmen, dass die =delete
-Syntax dafür sorgt, dass spezielle Funktionen nicht automatisch generiert werden. Tatsächlich verhalten sich die deleted
Funktionen aber anders. Am ehesten sind sie mit privaten Member-Funktionen vergleichbar, mit einigen wichtigen Unterschieden:
- Die Funktionen müssen und dürfen nicht definiert werden.
- Es ist sichergestellt, dass die Funktionen nicht aufgerufen werden können.
- Die Fehlermeldungen bei dem Versuch sie zu verwenden sind spezifischer.
- Und im Gegensatz zu privaten Funktionen können auch Funktionen außerhalb von Klassen als
deleted
gekennzeichnet werden.
Genauso wie private Funktionen nehmen aber auch die deleted
Funktionen an der Overload-Resolution teil. Damit kann man sie verwenden, um bestimmte Aufrufe zu vermeiden – insbesondere im Zusammenhang mit implizierten Konvertierungen.
// actual function
void write(double value);
// avoid invoking the function with an int argument
void write(int value) = delete;
int main()
{
write(42.0); // okay
write(42); // ERROR: this line does not compile
}
In diesem Beispiel wird verhindert, dass die Funktion write
mit einem int
-Argument aufgerufen wird. Damit das auch für alle anderen integralen Datentypen funktioniert, bietet es sich an, in diesem Fall ein template
zu definieren. Dadurch wird die nicht-gelöschte Funktion nur dann verwendet, wenn die Parameter genau passen.
// actual function
void write(double value);
// avoid invoking the function with all other argument types
template <typename Type>
void write(Type value) = delete;
Hilfreich ist der Mechanismus auch im Zusammenhang mit L‑Value- und R‑Value-References. In einigen Fällen erwarten Funktionen das Argument als Const-Referenz, die sie über den Lebenszeit des Funktionsaufrufs hinweg speichern. In diesen Fall wäre es gefährlich, ein temporäres Objekt an diese Referenz zu binden. Mit Hilfe der gelöschten Funktionen und der R‑Value-Referenzen lässt sich das nun einfach erreichen.
struct object { };
struct holder
{
const object& _ref;
explicit holder(const object& ref)
: _ref(ref)
{ }
// avoid invoking the constructor with a temporary
holder(const object&&) = delete;
};
Die deleted
-Funktionen bieten damit Möglichkeiten, die ich bisher so nicht gekannt habe. Und es fällt mir aktuell schwer zu beurteilen, in welchen Fällen ich davon Gebrauch machen werde. In jedem Fall werde ich bei neuem Code auf einen sinnvollen Einsatz achten und beobachten, welchen Nutzen sie tatsächlich bringen.