No New New: Das Ende von Zeigern in C++

Fünf Blogger haben sich weltweit zusammengetan und den Lesern einen Aprilscherz zur Abschaffung von Zeigern geliefert. Die Resonanz auf die fünf Artikel war riesig und reichte von "endlich" bis "das darf doch nicht wahr sein". Hier nochmals die Wahrheiten, Halbwahrheiten und Unwahrheiten.

In Pocket speichern vorlesen Druckansicht 38 Kommentare lesen
Lesezeit: 7 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Fünf Blogger haben sich weltweit zusammengetan und den Lesern einen Aprilscherz zur Abschaffung von Zeigern geliefert. Die Resonanz auf die fünf Artikel war riesig und reichte von "endlich" bis "das darf doch nicht wahr sein". Hier nochmals die Wahrheiten, Halbwahrheiten und Unwahrheiten.

Zuerst einmal, wer sind die fünf Blogger:

Vor zwei Wochen fand das ISO-C++-Standardisierungsmeeting in Jacksonville statt. Daher will ich heute einen kleinen Ausflug machen und über eine revolutionäre Entscheidung schreiben, die dort getroffen wurde. Zusätzlich beziehe ich mich auf den englischsprachigen Artikel "C++ will no Longer have Pointers" (Fluent C++). Das Standardisierungskomitee entschied, dass Zeiger mit C++20 deprecated und mit C++23 mit hoher Wahrscheinlichkeit entfernt werden.

Das, was sich wie ein Revolution anfühlt, ist tatsächlich nur der letzte Schritt in einer langen Evolution. Daher beschreibe ich erst einmal das große Bild:

Zeiger gibt es von Anfang an in C++. Das ist ein Erbe von C. Von Anfang an gab es auch den Versuch in C++, den Umgang von Zeigern ohne zusätzliche Kosten typsicherer zu machen.

Mit C++98 erhielten wir mit std::auto_ptr den ersten Smart Pointer. Mit diesem ließen sich exklusive Besitzverhältnisse ausdrücken. Aber std::auto_ptr besaß einen großen Nachteil. Wurde ein std::auto_ptr kopiert, wurde seine Ressource heimlich verschoben. Was sich wie eine Copy-Operation anfühlte, war unter der Decke eine Move-Operation. Die Grafik zeigt das überraschende Verhalten des std::auto_ptr.

Dieses Verhalten war sehr bösartig und der Grund für viele ernsthafte Bugs. Daher wurde C++11 um std::unique_ptr erweitert und std::auto_ptr in C++11 auf deprecated gesetzt und in C++17 entfernt. Zusätzlich erhielt C++11 mit dem std::shared_ptr und dem std::weak_ptr zwei Smart Pointer für geteilte Besitzverhältnisse. Ein std::unique_ptr kann nicht kopiert, aber verschoben werden. Falls du hingegen einen std::shared_ptr kopierst oder zuweist, wird der interne Referenzzähler erhöht. Die Grafiken bringen das Verhalten beider Smart Pointer auf den Punkt.

Seit C++11 besitzt C++ eine Multithreading-Bibliothek. Das macht den Umgang mit std::shared_ptr sehr anspruchsvoll, denn der std:.shared_ptr ist per Definition geteilt und nicht thread-sicher. Lediglich der Zugriff auf den Kontrollblock ist thread-sicher, aber nicht der Umgang mit seiner zugrunde liegenden Ressource. Das bedeutet, er bietet die Garantie, dass das Verändern des Referenzzählers eine atomare Operation ist und dass die Ressource genau einmal gelöscht wird. Dies ist der Grund, warum wir mit C++20 atomare Smart Pointer erhalten: std::atomic_shared_ptr und std::atomic_weak_ptr. Die Details dazu gibt es im Proposal: Atomic Smart Pointers.

Nun aber zum deutlich interessanteren Punkt der neuen C++20- und C++23-Standards. Zeiger werden in C++20 als deprecated erklärt und in C++23 entfernt. Um es in drei Worten zu sagen: No New New (NNN).

Aber halt. Wir haben einen Glaubenssatz in C++: Zahle nicht für etwas, das du nicht benutzt. Wie können wir daher auf Zeiger verzichten? Verwende einfach einen std::unique_ptr. Er ist per Design so schnell und so leichtgewichtig wie ein nackter Zeiger und besitzt einen großen Vorteil: Er passt automatisch auf seine Ressource auf.

Nur als Erinnerungsstütze. Hier ist ein kleiner Performanztest.

#include <chrono>
#include <iostream>

static const long long numInt= 100000000;

int main(){

auto start = std::chrono::system_clock::now();

for ( long long i=0 ; i < numInt; ++i){
int* tmp(new int(i));
delete tmp;
// std::shared_ptr<int> tmp(new int(i));
// std::shared_ptr<int> tmp(std::make_shared<int>(i));
// std::unique_ptr<int> tmp(new int(i));
// std::unique_ptr<int> tmp(std::make_unique<int>(i));
}

std::chrono::duration<double> dur= std::chrono::system_clock::now() - start;
std::cout << "time: " << dur.count() << " seconds" << std::endl;

}

Das Programm fordert und gibt 100.000.000 ints dynamisch an. Dazu kommen nackte Zeiger und die Smart Pointer std::shared_ptr und std::unique_ptr in zwei Variationen zum Einsatz. Ich übersetzte dann noch das Programm auf Windows und Linux mit maximaler beziehungsweise ohne Optimierung und führe es aus. Hier sind die überzeugenden Zahlen:

Die Zahlen zeigen es schwarz auf blau. Die zwei Variationen des std::unique_ptr sind auf Linux und Windows so schnell wie nackte Zeiger. Die Details zu den Zahlen gibt es auf meinem Artikel "Speicher und Performanz Overhead von Smart Pointern".

Ehrlich gesagt verwenden wir Zeiger und insbesondere nackte Zeiger viel zu häufig. Die Frage, ob du einen Zeiger verwenden solltest, lässt sich auf eine einfache Frage reduzieren: Wer ist der Besitzer? Glücklicherweise können wir mit modernem C++ unsere Besitzverhältnisse direkt im Sourcecode ausdrücken.

  • Lokale Objekte: Die C++-Laufzeit als Besitzer verwaltet automatisch den Lebenszyklus seiner Ressourcen. Dasselbe gilt für globale Objekte oder Mitglieder einer Klasse. Die Guidelines nennen diese lokalen Objekte scoped objects.
  • Referenzen: Ich bin nicht der Besitzer. Ich habe mir die Ressource, die nicht null sein kann, nur ausgeliehen.
  • Nackte Zeiger: Ich bin nicht der Besitzer. Ich habe mir die Ressource nur ausgeliehen. Ich darf die Ressource nicht freigeben.
  • std::unique_ptr: Ich bin der exklusive Besitzer der Ressource. Ich darf die Ressource freigeben.
  • std::shared_ptr: Ich teile mir die Ressource mit anderen Besitzern. Ich darf meine Besitzverhältnisse explizit freigeben.
  • std::weak_ptr: Ich bin nicht der Besitzer der Ressource, aber ich kann zeitweise zum geteilten Besitzer werden, indem ich die Methode std::weak_ptr::lock verwende.

Daher müssen wir unsere Art zu programmieren nur in einem von sechs Anwendungsfällen ändern, und wir sind bereits auf der nächsten Evolutionsstufe von C++.

Nochmals in aller Deutlichkeit: Zeiger werden nicht in C++20 deprecated und in C++23 entfernt werden.

Sorry für diesen Ausflug von meinem ursprünglichen Plan, mich mit den C++ Core Guidelines zu befassen, aber die Entscheidung in Jacksonville ist einfach zu wichtig. Mein nächster Artikel wird sich wieder mit den Guidelines beschäftigen. Die verbleibenden Regeln zur Performanz stehen an.

Die Entscheidung zum nächsten PDF-Päckchen ist gefallen. Ungefähr eine Woche werde ich benötigen, um beide PDF-Päckchen zu veröffentlichen. Zuerst muss ich sie allerdings noch überarbeiten.

()