Was ist neun multipliziert mit sechs?
Muss man als Entwickler eigentlich die grundlegenden Dinge von Programmiersprachen verstanden haben? Ich glaube nicht. Denn offensichtlich reicht eine gewisse Abstraktionsebene aus. Dennoch finde ich es interessant, einen genaueren Blick auf die Details zu werfen. Einerseits natürlich, weil in vielen Situationen Details entscheidend sein können. Andererseits und insbesondere aber auch, weil sich viele Konzepte von den Grundlagen auf höhere Abstraktionsebenen übertragen lassen.
Was hat es beispielsweise mit folgender Anweisung in C++ auf sich?
9 * 6 = 42;
Die Anweisung sieht ein wenig merkwürdig aus – und überhaupt nicht sinnvoll. Daher ist es kaum überraschend, dass sie ungültig ist und der Compiler eine Fehlermeldung liefert. Allerdings nicht aufgrund der (kontextfreien) Syntax: Die Anweisung ist ungültig, weil es keinen Zuweisungsoperator für den Typ auf der linken Seite des Gleichheitszeichens gibt.
Ich finde das gar nicht so offensichtlich: Es ist ja nicht so, dass hier versucht wird, einem Ausdruck ein Ergebnis zuzuweisen. Viel eher kann man sagen: Es wird der Operator für die Multiplikation aufgerufen, der ein int
-Objekt zurückliefert, auf dem anschließend der Operator für die Zuweisung aufgerufen wird. Als Code sieht das so aus:
operator*(9, 6).operator=(42);
Dass es trotzdem nicht funktioniert hat etwas mit L‑Values und R‑Values zu tun. Dabei handelt es sich nicht um unterschiedliche Typen sondern vielmehr um unterschiedliche Sichten auf die selben Typen. Und: Man kann zwischen den Sichten wechseln. Ich zeige gleich konkret wie das aussieht, verwende zunächst aber einfach die Schreibweise _(...)
für den Wechsel von R‑Value- zu L‑Value-Sicht. Damit ist die folgende Anweisung korrekt – wenn auch immer noch sinnlos und ohne irgendeinen Effekt:
_( 9 * 6 ) = 42; // OK
Genauso kann man auch die symmetrische Anweisung betrachten. Auch diese Anweisung ist ungültig und wird durch den Wechsel von R‑Value- zu L‑Value-Sicht korrekt:
42 = 9 * 6; // ERROR
_( 42 ) = 9 * 6; // OK
Eigentlich ist der Fall identisch zum ersten Fall. Trotzdem ist er ein wenig irritierend. Das liegt daran, dass vielen Entwicklern nicht vollständig klar ist, was 42
überhaupt ist. Nein, 42
ist keine Konstante. 42
ist ein int
-Literal, und logisch betrachtet handelt es sich dabei um einen Funktionsaufruf, der bei jeder Verwendung ein neues int
-Objekt liefert. Also passiert im Prinzip genau das Gleiche wie oben: Nicht der Wert von 42
wird definiert, sondern es wird lediglich der Zuweisungsoperator an einem temporären int
-Objekt aufgerufen – ohne Sinn und Effekt.
Fehlt noch dieses magische _(...)
. Dabei handelt es sich um ein Funktionstemplate, das ich nun besser mit as_lvalue_reference
bezeichne, und das wie folgt aussieht:
template <typename Type>
auto as_lvalue_reference(Type&& ref) -> Type&
{
return ref;
}
Eigentlich ist dieses Ding ganz einfach. Ich fasse es trotzdem nochmals in Worte: Man kann in C++ keine Objekte übergeben. Stattdessen übergibt man entweder Referenzen auf Objekte oder erzeugt neue Objekte. Dieses Funktionstemplate kann für alle Referenztypen passend instantiiert werden, und der einzige Effekt ist, dass es immer eine L‑Value-Referenz auf das durch den Parameter referenzierte Objekt zurückgibt.
Wie gesagt: Das ist alles ganz einfach. Und wer weiß schon was wäre, wenn unsere Welt nicht von Unternehmensberatern, Telefondesinfizierern und Frisören geprägt wäre.