C++ Insights: Implizite Konvertierungen

Dieser Artikel ist der Auftakt einer Miniserie zu Artikel zu C++ Insights. Dieses ist ein großartiges Werkzeug, mit dem unser Autor in seinen Artikeln und Schulungen die Magie der C++-Compiler offenlege.

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

Dieser Artikel ist der Auftakt einer Miniserie zu Artikel zu C++ Insights. Dieses ist ein großartiges Werkzeug, mit dem ich in meinen Artikeln und Schulungen die Magie der C++-Compiler offenlege.

Diese Serie ist durch ein kurzes Gespräch motiviert, das Andreas Fertig und ich hatten. Ich fragte ihn, ob er Anwendungsbeispiele habe, um zu zeigen, wie C++ Insights beim Unterrichten hilfreich sein kann. Ich denke es gibt viele Beispiele. Dieser Artikel ist der Beginn einer Serie von fünf Beiträgen von Andreas, die ich hier veröffentlichen werde. Wer mit C++ Insights noch nicht vertraut ist, dem empfehle ich diesen einleitenden Artikel. Ohne weitere Vorworte, hier ist Andreas' Artikel. Wenn du dem jeweiligen Link bei den Beispielen folgst, kannst du das Beispiel direkt in C++ Insight analysieren:

Beginnen wir mit etwas Einfachem, das so oft vorkommt: implizite Konvertierungen. Manchmal werden sie als undurchsichtig oder verborgen wahrgenommen, manchmal als mächtig. Für Anfänger und sogar für Experten in es in bestimmten Debugging-Situationen ist sehr schwer zu erkennen, wo implizite Konvertierungen stattfinden. Betrachten wir dieses grundlegende Beispiel:

void UnsignedIntFunction(unsigned int) {}

int main()
{
int x = 1;
UnsignedIntFunction(x);
}

Mit diesen wenigen Zeilen und Kenntnissen in C++ ist es leicht zu sehen, dass UnsignedIntFunction einen unsigned int verwendet, während wir ein int übergeben. Abgesehen von der Tatsache, dass die beiden Typen auf der Anrufseite unterschiedliche Bereiche haben, funktioniert es ohne zusätzlichen Arbeitsaufwand. Diese Verringerung des Wertebereichs ist in einer größeren Codebasis schwieriger zu erkennen. Für Studenten ist es in meiner Erfahrung noch schwieriger. Mit C++ Insights erhält man die folgende Ausgabe:

void UnsignedIntFunction(unsigned int)
{
}


int main()
{
int x = 1;
UnsignedIntFunction(static_cast<unsigned int>(x));
}

Wir können jetzt zeigen, wo die implizite Konvertierung beginnt. Der große Vorteil von C++ Insights als Online-Tool ist, dass wir die Signatur von UnsignedIntFunction in int ändern und die implizite Konvertierung verschwinden lassen können. Dies ist mächtig, da Studenten beziehungsweise Schulungsteilnehmer nicht nur uns glauben müssen, sie können selbst damit experimentieren. Ich sage meinen Studenten oft, sie sollen mir nicht vertrauen, sondern hinterfragen, was ich ihnen sage. Mit C++ Insights können sie das problemlos. Leider finden sie manchmal meine Fehler, aber das ist eine andere Geschichte.

Schauen wir uns etwas Komplexeres wie Klassen mit Konvertierungsoperatoren an, die etwas schwieriger zu erkennen sind. An dieser Stelle haben wir Operatorenüberladung dem Bilde hinzugefügt. Die Frage ist, welche Überladung wann ausgewählt wird. Kürzlich gab es einen Tweet von @walletfox, der zeigt, wie C++ Insights zeigen kann, welche Überladung von std::cout ausgewählt wird. Betrachten wir dieses cout-freie Beispiel:

class Foo
{
public:
Foo(int v)
: mV{v}
{
}

Foo& operator+=(const Foo& rhs)
{
mV += rhs.mV;
return *this;
}

operator int() { return mV; }

private:
int mV;
};

int main()
{
Foo t(2);
t = 3 * 2;

Foo tt(4);
t += tt * 2;
}

Die Frage ist, ob wer direkt erkennt, was für t = 3 * 2 und t + = tt * 2 geschieht. Wenn wir C++ Insights ausführen, erhalten wir folgende Ausgabe:

class Foo
{
public:
inline Foo(int v)
: mV{v}
{
}



inline Foo & operator+=(const Foo & rhs)
{
this->mV += rhs.mV;
return *this;
}


using retType = int;
inline operator retType ()
{
return this->mV;
}


private:
int mV;
/* public: inline constexpr Foo(const Foo &); */
/* public: inline constexpr Foo(Foo &&); */
/* public: inline Foo & operator=(const Foo &); */
/* public: inline Foo & operator=(Foo &&) noexcept; */
/* public: inline ~Foo() noexcept; */
};

int main()
{
Foo t = Foo(2);
t.operator=(Foo(3 * 2));
Foo tt = Foo(4);
t.operator+=(Foo(static_cast<int>(tt.operator int()) * 2));
}

Neben der Frage können wir implizite Member-Funktionen sehen, die der Compiler für uns generiert. Wie die Kopier- und Verschiebungsoperationen sowie den Destruktor. Wir sehen auch die Antwort auf die ursprüngliche Frage, es gibt implizite Konvertierungen. Beide Male wird ein temporäres Objekt von Foo erstellt, das dann an operator=(const Foo&) und operator+=(const Foo) übergeben wird. Wir haben den ersten Operator nicht implementiert. Darüber hinaus wird der Konvertierungsoperator operator int() verwendet, um ein Foo-Objekt zunächst in einen int zu konvertieren, einfach mit 2 zu multiplizieren und das Ergebnis dann in einem temporären Foo-Objekt zu übergeben.

Eine andere Sache, die uns C++ Insights zeigt, sind die speziellen Member-Funktionen, die der Compiler für uns generiert. Im obigen Beispiel sehen wir den Kopier- und Move-Konstruktor sowie den Zuweisungs- und Move-Operator. Hier ein Beispiel, das es noch besser demonstriert:

class A
{
public:
A() = default;
A(const A&) {}
};

class B
{
public:
};

int main()
{
A a;
A a2;
//a = a2;

B b;
}

In Klasse A stellen wir einen Kopierkonstruktor bereit. Damit generiert der Compiler die Move-Operationen für diese Klasse nicht mehr wie für B:

class A
{
public:
A() = default;
inline A(const A &)
{
}


// public: inline constexpr A() noexcept;
};

class B
{
public:
// public: inline constexpr B() noexcept;
// public: inline constexpr B(const B &);
// public: inline constexpr B(B &&);
};

int main()
{
A a = A();
A a2 = A();
B b = B();
}

Was wir zusätzlich sehen können, ist, dass die speziellen Member nur bei Bedarf generiert werden. Im vorliegenden Code gibt es keinen Zuweisungs-Operator. Wenn wir jedoch die Zeile a = a2 aktivieren, erhalten wir einen:

class A
{
public:
A() = default;
inline A(const A &)
{
}


// public: inline constexpr A() noexcept;
// public: inline constexpr A & operator=(const A &) noexcept;
};

class B
{
public:
// public: inline constexpr B() noexcept;
// public: inline constexpr B(const B &);
// public: inline constexpr B(B &&);
};

int main()
{
A a = A();
A a2 = A();
a.operator=(a2);
B b = B();
}

Ich denke, die Stärke von C++ Insights besteht darin, dass wir sehen können, wie sich unser Code ändert, was der Compiler hinzufügt oder auswählt. Es ist mehr oder weniger wie der geniale Compiler Explorer, außer dass er das Ergebnis in einer Sprache ausspuckt, die wir alle gut verstehen.

Demnächst mehr zu C++ Insights zu Typableitung ... ()