C++ Insights: Automatische Typableitung

Andreas Fertigs Serie über C++ Insights geht weiter. Dieser Artikel beschäftigt sich mit der automatischen Typableitung mithilfe von auto und decltype oder wie es sich gut umschreiben lässt: "Nutze die Intelligenz des Compilers."

In Pocket speichern vorlesen Druckansicht
Lesezeit: 7 Min.
Von
  • Andreas Fertig
  • Rainer Grimm
Inhaltsverzeichnis

Andreas Fertigs Serie über C++ Insights geht weiter. Dieser Artikel beschäftigt sich mit der automatischen Typableitung mithilfe von auto und decltype oder wie ich es gerne umschreibe: "Nutze die Intelligenz des Compilers."

Mit C++ 11 bekamen wir auto und decltype, eine neue Form der Typermittlung.

Wir sind Typermittlung von Templates gewohnt, jedoch können diese beiden neuen Varianten manchmal schwierig sein. Betrachtet sei dieses Beispiel:

int main()
{
int* ip;
const int* cip;
const int* const cicp = ip;

auto aip = ip;
auto acip = cip;
auto acicp = cicp;
}

Wir haben drei verschiedene Zeiger, alle vom Typ int. Sie werden immer mehr const. Die Frage ist, welche Art von auto abgeleitet wird. Alle sind Zeiger, das ist sicher. Aber was passiert mit dem const? Alle Qualifikationsmerkmale der obersten Ebene werden automatisch entfernt. Daher verschwindet auch das const. Stimmt das? Hier ist die Ausgabe, die C++ Insights liefert:

int main()
{
int * ip;
const int * cip;
const int *const cicp = ip;
int * aip = ip;
const int * acip = cip;
const int * acicp = cicp;
}

Ja, das const der obersten Ebene wird entfernt. Ein konstanter Zeiger spielt keine Rolle, also wird dieses const verworfen, aber die Konstanz des dahinterliegenden Speichers bleibt bestehen. Daher bleibt dieses const erhalten. Deshalb sieht acip genau wie acicp aus. Macht Sinn, richtig?

Von Zeit zu Zeit möchten wir alle Qualifikanten beibehalten. Dies ist der Zeitpunkt, an dem decltype erscheint. Im Gegensatz zu auto behält decltype alle Top-Level-Qualifikationsmerkmale bei. Mit C++14 ist die Kombination von decltype und auto möglich, wir können decltype(auto) schreiben, was die Sache einfacher macht. Hier ist ein weiteres Beispiel aus C++ Insights, das C++14 nutzt:

int main()
{
int* ip;
const int* cip;
const int* const cicp = ip;

decltype(auto) aip = ip;
decltype(auto) acip = cip;
decltype(auto) acicp = cicp;
}

Daraus erhalten wir folgende Ausgabe:

int main()
{
int * ip;
const int * cip;
const int *const cicp = ip;
int * aip = ip;
const int * acip = cip;
const int *const acicp = cicp;
}

Wir können sehen, dass acicp das zweite const trägt, das verloren geht, wenn wir nur auto verwenden.

Wann brauchen wir decltype oder genauer gesagt, wann möchten wir alle Qualifikanten behalten? Ein beliebter Grund sind Templates. Stellen wir uns ein Klassen-Template mit der Funktion Get vor. Wenn wir nur auto als Rückgabetyp verwenden, können wir niemals einen Verweis auf etwas zurückgeben. Im Template-Code kennen wir oft nicht die genauen Typen, weshalb es wünschenswert ist, Code bereitzustellen, der gerade funktioniert. Hier kann decltype helfen. Wir sollten es jedoch als Funktionalität für Bibliotheksautoren betrachten. In den meisten Fällen geht es uns mit auto gut. Es ist nur einfach gut, die gesamte Toolbox genau zu kennen.

Was wir bisher gesehen haben, ist, dass wir explizit sein müssen, wenn es um auto und Referenzen geht. Wir müssen immer auto& schreiben, um eine Referenz zu erhalten. Wie ist es mit Zeigern? Gibt uns auto den richtigen Typ und können wir uns den Stern sparen? Dies ist in der Tat eine Frage, die ich häufig von meinen Schülern bekomme. Die Antwort ist: Es kommt darauf an. Ich empfehle es, schon aus Gründen der Konsistenz zu schreiben. Es gibt jedoch Szenarien, in denen wir auto* benötigen, selbst wenn automatisch der korrekte Typ abgeleitet wurde. Betrachten wir dieses Beispiel:

struct Foo{};

Foo* GetFoo()
{
static Foo foo;

return &foo;
}

int main()
{
auto fp = GetFoo();
}

Wir haben eine Funktion, die ein Foo* zurückgibt, und eine auto-Variable auto f = GetFoo(), die den Typ ableitet. Natürlich der richtige Typ. Was ist, wenn wir gerne f const machen? Dass wir die Daten von f nicht ändern können? Sicher schreiben wir es so: const auto f = ... Zumindest würden wir das tun, wenn wir es ohne auto schreiben würden. Hier sind einige Möglichkeiten, die wir ausprobieren können:

struct Foo{};

Foo* GetFoo()
{
static Foo foo;

return &foo;
}

int main()
{
auto fp0 = GetFoo();
const auto fp1 = GetFoo();
auto const fp2 = GetFoo();
//const auto const fp3 = GetFoo(); does not compile
const auto* fp4 = GetFoo();
auto* const fp5 = GetFoo();
const auto* const fp6 = GetFoo();
}

Zunächst erzeugt fp1 einen const-Zeiger auf veränderliche Daten. Nicht genau das, was wir beabsichtigten. fp2 scheint wahrscheinlich sinnlos. Am sinnvollsten ist fp3, aber das wird nicht kompiliert. Die Kontrolle ändert sich, wenn wir die Form auto* verwenden. Jetzt können wir die Qualifier hinzufügen, wie wir es mit einem regulären Typ tun können. Aber sehen wir selbst in C++ Insights, was das Ergebnis ist:

struct Foo{/* public: inline constexpr Foo() noexcept; */
/* public: inline constexpr Foo(const Foo &); */
/* public: inline constexpr Foo(Foo &&); */
};

Foo * GetFoo()
{
static Foo foo = Foo();
return &foo;
}


int main()
{
Foo * fp0 = GetFoo();
Foo *const fp1 = GetFoo();
Foo *const fp2 = GetFoo();
const Foo * fp4 = GetFoo();
Foo *const fp5 = GetFoo();
const Foo *const fp6 = GetFoo();
}

Der einfache Rat ist, immer explizit zu bleiben und das Formular auto& sowie auto* zu verwenden, auch wenn auto einen Zeigertyp ableiten kann.

Nehmen wir an, wir sind explizit und verwenden auto&. Schauen wir uns dieses Beispiel an:

struct Singleton{};

auto Get()
{
static Singleton s{};

return s;
}

int main()
{
auto& x = Get();
}

Wir haben ein klassisches Singleton, das die Funktion Get zurückgeben soll. Natürlich möchten wir eine Referenz darauf, ansonsten haben wir mehrere Singletons. Trotz auto und & kompiliert dieser Code nicht:

error: non-const lvalue reference to type 'Singleton' cannot bind to a temporary of type 'Singleton'

Der Grund ist, dass Get tatsächlich Singleton zurückgibt und nicht Singleton&. Warum? Weil wir das & nicht auf den automatischen Rückgabetyp von Get angewendet haben. Eine kleine Änderung und der Code kompiliert:

struct Singleton{};

auto& Get()
{
static Singleton s{};

return s;
}

int main()
{
auto& x = Get();
}

Viel Spaß mit C++ Insights. Wer möchte, kann das Projekt unterstützen, entweder als Patreon oder natürlich auch mit Code-Beiträgen.

Demnächst mehr zu C++ Insights und Template Instanziierungen ...

Auf meinem deutschen und meinem englischen Blog findet gerade die Wahl zum nächsten PDF-Päckchen statt. Hier sind die Links zur Wahl: