Ableiten this
(P0847) ist eine C++23-Funktion, die eine neue Möglichkeit zur Angabe nicht statischer Memberfunktionen bietet. Wenn wir die Mitgliedsfunktion eines Objekts aufrufen, ist das Objekt normalerweise implizit an die Memberfunktion übergeben, obwohl er nicht in der Parameterliste vorhanden ist. Mit P0847 können wir diesen Parameter festlegen explizitihm einen Namen geben und const
/reference-Qualifizierer. Zum Beispiel:
struct implicit_style {
void do_something(); //object is implicit
};
struct explicit_style {
void do_something(this explicit_style& self); //object is explicit
};
Der explizite Objektparameter wird durch das Schlüsselwort unterschieden this
wird vor dem Typbezeichner platziert und ist nur für den ersten Parameter der Funktion gültig.
Die Gründe dafür, dies zuzulassen, scheinen vielleicht nicht sofort offensichtlich zu sein, aber eine Reihe zusätzlicher Funktionen ergeben sich dadurch fast wie von Zauberhand. Dazu gehören Dequadruplikation von Code, rekursive Lambdas und Übergabe this
nach Wert und eine Version des CRTP, die nicht erfordert, dass die Basisklasse auf der abgeleiteten Klasse als Vorlage erstellt wird.
In diesem Beitrag erhalten Sie einen Überblick über das Design und anschließend viele Fälle, in denen Sie diese Funktion in Ihrem eigenen Code verwenden können.
Für den Rest dieses Blogbeitrags werde ich die Funktion als „explizite Objektparameter“ bezeichnen, da sie als Funktionsname sinnvoller ist als als „Ableitung“. this
„. Explizite Objektparameter werden in MSVC ab Visual Studio 2022 Version 17.2 unterstützt. Ein guter Begleiter zu diesem Beitrag ist Ben Deanes Vortrag Deducing this
Muster von CppCon.
Überblick
Der Artikel, in dem diese Funktion vorgeschlagen wurde, wurde von verfasst Gašper Ažman, Ben Deane, Barry RevzinUnd ich selbstund wurde von der Erfahrung vieler Experten auf diesem Gebiet geleitet. Barry und ich begannen, eine Version dieses Papiers zu schreiben, nachdem wir es jeweils umgesetzt hatten std::optional
und bin auf das gleiche Problem gestoßen. Wir würden das schreiben value
Die Funktion von optional
und wie gute Bibliotheksentwickler würden wir versuchen, sie in so vielen Anwendungsfällen wie möglich nutzbar und leistungsfähig zu machen. Also würden wir wollen value
a zurückgeben const
Referenz, wenn das Objekt, für das es aufgerufen wurde, war const
wir möchten, dass es einen R-Wert zurückgibt, wenn das Objekt, für das es aufgerufen wurde, ein R-Wert usw. war. Am Ende sah es so aus:
template
class optional {
// version of value for non-const lvalues
constexpr T& value() & {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}
// version of value for const lvalues
constexpr T const& value() const& {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}
// version of value for non-const rvalues... are you bored yet?
constexpr T&& value() && {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}
// you sure are by this point
constexpr T const&& value() const&& {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}
// ...
};
(Wenn Sie mit dem nicht vertraut sind member_function_name() &
Diese Syntax wird als „Ref-Qualifizierer“ bezeichnet. Weitere Informationen finden Sie im Blog von Andrzej Krzemieński. Wenn Sie mit R-Wert-Referenzen nicht vertraut sind (T&&
) Sie können sich in dieser Frage zum Stapelüberlauf über die Bewegungssemantik informieren.)
Beachten Sie die nahezu identischen Implementierungen von vier Versionen derselben Funktion, die sich nur dadurch unterscheiden, ob sie es sind const
und ob sie den gespeicherten Wert verschieben, anstatt ihn zu kopieren.
Barry und ich gingen dann zu einer anderen Funktion über und mussten dasselbe tun. Und immer und immer wieder Code duplizieren, Fehler machen und Wartungsprobleme für zukünftige Versionen von uns selbst verursachen. „Was wäre, wenn“, dachten wir, „Sie das einfach schreiben könnten?“
template
struct optional {
// One version of value which works for everything
template
constexpr auto&& value(this Self&& self) {
if (self.has_value()) {
return std::forward(self).m_value;
}
throw bad_optional_access();
}
(Wenn Sie nicht damit vertraut sind std::forward
über Perfect Forwarding können Sie auf Eli Benderskys Blog lesen)
Dies bewirkt dasselbe wie die oben genannten vier Überladungen, jedoch in einer einzigen Funktion. Anstatt verschiedene Versionen von zu schreiben value
für const optional&
, const optional&&
, optional&
Und optional&&
wir schreiben eine Funktionsvorlage, die leitet ab Die const
/volatile
/reference (kurz cvref) Qualifizierer des Objekts, für das es aufgerufen wird. Diese Änderung für fast jede Funktion im Typ würde unseren Code enorm verkürzen.
Also schrieben wir eine Version dessen, was schließlich standardisiert wurde, stellten bald fest, dass Gašper und Ben an einem anderen Papier für genau dasselbe Feature arbeiteten, schlossen uns zusammen und hier sind wir alle einige Jahre später.
Design
Das wichtigste Designprinzip, dem wir folgten, war, dass es so sein sollte Mach, was du erwartest. Um dies zu erreichen, haben wir so wenige Stellen im Standard wie möglich berührt. Insbesondere haben wir die Überlastungsauflösungsregeln oder Vorlagenabzugsregeln nicht berührt und die Namensauflösung wurde nur geringfügig geändert (als Leckerbissen).
Angenommen, wir haben einen Typ wie diesen:
struct cat {
template
void lick_paw(this Self&& self);
};
Der Vorlagenparameter Self
wird basierend auf denselben Vorlagenabzugsregeln abgeleitet, mit denen Sie bereits vertraut sind. Es gibt keine zusätzliche Magie. Sie müssen die Namen nicht verwenden Self
Und self
aber ich denke, das sind die klarsten Optionen, und dies folgt dem, was mehrere andere Programmiersprachen tun.
cat marshmallow;
marshmallow.lick_paw(); //Self = cat&
const cat marshmallow_but_stubborn;
marshmallow_but_stubborn.lick_paw(); //Self = const cat&
std::move(marshmallow).lick_paw(); //Self = cat
std::move(marshmallow_but_stubborn).lick_paw(); //Self = const cat
Eine Änderung der Namensauflösung besteht darin, dass Sie innerhalb einer solchen Memberfunktion weder explizit noch implizit darauf verweisen dürfen this
.
struct cat {
std::string name;
void print_name(this const cat& self) {
std::cout << name; //invalid
std::cout << this->name; //also invalid
std::cout << self.name; //all good
}
};
Anwendungsfälle
Für den Rest dieses Beitrags werden wir uns die verschiedenen Einsatzmöglichkeiten dieser Funktion ansehen (zumindest die bisher entdeckten, soweit ich weiß!). Viele dieser Beispiele stammen direkt aus der Arbeit.
Deduplizierung/Vervierfachung
Wir haben bereits gesehen, wie die Funktion auf einen Typ wie angewendet werden kann optional
um zu vermeiden, dass vier Überladungen derselben Funktion geschrieben werden müssen.
Beachten Sie auch, dass dies den Aufwand für die anfängliche Implementierung und Wartung des Umgangs mit R-Wert-Memberfunktionen verringert. Sehr oft schreiben Entwickler nur const
und nicht-const
Überladungen für Mitgliedsfunktionen, da wir in vielen Fällen nicht wirklich zwei weitere ganze Funktionen schreiben möchten, nur um mit R-Werten umzugehen. Mit abgeleiteten Qualifikationsmerkmalen this
erhalten wir die Rvalue-Versionen kostenlos: Wir müssen nur schreiben std::forward
an den richtigen Stellen, um die Laufzeitleistungssteigerungen zu erzielen, die mit der Vermeidung unnötiger Kopien einhergehen:
class cat {
toy held_toy_;
public:
//Before explicit object parameters
toy& get_held_toy() { return held_toy_; }
const toy& get_held_toy() const { return held_toy_; }
//After
template
auto&& get_held_toy(this Self&& self) {
return self.held_toy_;
}
//After + forwarding
template
auto&& get_held_toy(this Self&& self) {
return std::forward(self).held_toy_;
}
};
Bei einem einfachen Getter wie diesem liegt es natürlich bei Ihnen, ob sich diese Änderung für Ihren speziellen Anwendungsfall lohnt oder nicht. Aber bei komplexeren Funktionen oder Fällen, in denen Sie es mit großen Objekten zu tun haben, die Sie nicht kopieren möchten, erleichtern explizite Objektparameter die Handhabung erheblich.
CRTP
Das Curiously Recurring Template Pattern (CRTP) ist eine Form des Polymorphismus zur Kompilierungszeit, der es Ihnen ermöglicht, Typen mit gemeinsamen Funktionalitätsteilen zu erweitern, ohne die Laufzeitkosten virtueller Funktionen zu bezahlen. Dies wird manchmal als bezeichnet Mixins (Das ist nicht der Fall alle das CRTP kann verwendet werden, ist aber die häufigste Verwendung). Wir könnten zum Beispiel einen Typ schreiben add_postfix_increment
Dies kann in einen anderen Typ eingemischt werden, um das Postfix-Inkrement als Präfix-Inkrement zu definieren:
template
struct add_postfix_increment {
Derived operator++(int) {
auto& self = static_cast(*this);
Derived tmp(self);
++self;
return tmp;
}
};
struct some_type : add_postfix_increment {
// Prefix increment, which the postfix one is implemented in terms of
some_type& operator++();
};
Erstellen einer Vorlage für eine Basisklasse anhand ihrer abgeleiteten Umwandlung und static_cast
ing this
Das Innere der Funktion kann etwas geheimnisvoll sein, und das Problem wird schlimmer, wenn Sie über mehrere CRTP-Ebenen verfügen. Mit expliziten Objektparametern, da wir die Vorlagenabzugsregeln nicht geändert haben, Der Typ des expliziten Objektparameters kann auf einen abgeleiteten Typ abgeleitet werden. Konkreter:
struct base {
template
void f(this Self&& self);
};
struct derived : base {};
int main() {
derived my_derived;
my_derived.f();
}
Im Anruf my_derived.f()
die Art von Self
innen f
Ist derived&
, nicht base&
.
Das bedeutet, dass wir das obige CRTP-Beispiel wie folgt definieren können:
struct add_postfix_increment {
template
auto operator++(this Self&& self, int) {
auto tmp = self;
++self;
return tmp;
}
};
struct some_type : add_postfix_increment {
// Prefix increment, which the postfix one is implemented in terms of
some_type& operator++();
};
Beachten Sie das jetzt add_postfix_increment
ist keine Vorlage. Stattdessen haben wir die Anpassung in den Postfix verschoben operator++
. Das bedeutet, dass wir nicht passieren müssen some_type
als Musterargument überall: „Alles funktioniert einfach“.
Weiterleitung aus Lambdas
Das Kopieren erfasster Werte aus einem Abschluss ist einfach: Wir können das Objekt einfach wie gewohnt weitergeben. Das Verschieben erfasster Werte aus einem Abschluss ist ebenfalls einfach: Wir können einfach aufrufen std::move
darauf. Ein Problem tritt auf, wenn wir einen erfassten Wert perfekt weiterleiten müssen, basierend darauf, ob der Abschluss ein l-Wert oder ein r-Wert ist.
Ein Anwendungsfall, den ich aus P2445 geklaut habe, betrifft Lambdas, die sowohl im „Retry“- als auch im „Try or Fail“-Kontext verwendet werden können:
auto callback = [m=get_message(), &scheduler]() -> bool {
return scheduler.submit(m);
};
callback(); // retry(callback)
std::move(callback)(); // try-or-fail(rvalue)
Die Frage hier ist: Wie gehen wir weiter? m
basierend auf der Wertkategorie des Verschlusses? Explizite Objektparameter geben uns die Antwort. Da ein Lambda eine Klasse mit einem generiert operator()
Mitgliedsfunktion der gegebenen Signatur, alle Maschinen, die ich gerade erklärt habe, funktionieren auch für Lambdas.
auto closure = [](this auto&& self) {
//can use self inside the lambda
};
Dies bedeutet, dass wir basierend auf der Wertkategorie des Abschlusses innerhalb des Lambda eine Perfektion durchführen können. P2445 gibt eine std::forward_like
Helfer, der einen Ausdruck basierend auf der Wertkategorie eines anderen weiterleitet:
auto callback = [m=get_message(), &scheduler](this auto &&self) -> bool {
return scheduler.submit(std::forward_like(m));
};
Jetzt funktioniert unser ursprünglicher Anwendungsfall und das erfasste Objekt wird kopiert oder verschoben, je nachdem, wie wir den Verschluss verwenden.
Rekursive Lambdas
Da wir jetzt die Möglichkeit haben, das Abschlussobjekt in der Parameterliste eines Lambdas zu benennen, können wir rekursive Lambdas erstellen! Wie oben:
auto closure = [](this auto&& self) {
self(); //just call ourself until the stack overflows
};
Dafür gibt es allerdings noch sinnvollere Einsatzmöglichkeiten als nur überlaufende Stapel. Denken Sie zum Beispiel an die Möglichkeit, rekursive Datenstrukturen zu besuchen, ohne zusätzliche Typen oder Funktionen definieren zu müssen? Gegeben sei die folgende Definition eines Binärbaums:
struct Leaf { };
struct Node;
using Tree = std::variant;
struct Node {
Tree left;
Tree right;
};
Wir können die Anzahl der Blätter folgendermaßen zählen:
int num_leaves(Tree const& tree) {
return std::visit(overload( //see below
[](Leaf const&) { return 1; },
[](this auto const& self, Node* n) -> int {
return std::visit(self, n->left) + std::visit(self, n->right);
}
), tree);
}
overload
Hier finden Sie eine Möglichkeit zum Erstellen eines Überladungssatzes aus mehreren Lambdas, die häufig für verwendet wird variant
Heimsuchung. Siehe zum Beispiel cppreference.
Dabei wird die Anzahl der Blätter im Baum durch Rekursion gezählt. Für jeden Funktionsaufruf im Aufrufdiagramm, wenn der Strom a ist Leaf
es kehrt zurück 1
. Andernfalls ruft sich der überladene Abschluss selbst auf self
und führt eine Rekursion durch, wobei die Blattanzahlen für den linken und rechten Teilbaum addiert werden.
Passieren this
nach Wert
Da wir die Qualifizierer des jetzt expliziten Objektparameters definieren können, können wir ihn nach Wert statt nach Referenz verwenden. Bei kleinen Objekten kann dies zu einer besseren Laufzeitleistung führen. Falls Sie nicht wissen, wie sich dies auf die Codegenerierung auswirkt, finden Sie hier ein Beispiel.
Angenommen, wir haben diesen Code, der normale alte implizite Objektparameter verwendet:
struct just_a_little_guy {
int how_smol;
int uwu();
};
int main() {
just_a_little_guy tiny_tim{42};
return tiny_tim.uwu();
}
MSVC generiert die folgende Assembly:
sub rsp, 40
lea rcx, QWORD PTR tiny_tim$[rsp]
mov DWORD PTR tiny_tim$[rsp], 42
call int just_a_little_guy::uwu(void)
add rsp, 40
ret 0
Ich werde das Zeile für Zeile durchgehen.
sub rsp, 40
weist 40 Bytes auf dem Stapel zu. Dies sind 4 Bytes zum Speichernint
Mitglied vontiny_tim
32 Bytes von Schattenraum füruwu
zu verwendenden Datei und 4 Bytes Auffüllung.- Der
lea
Die Anweisung lädt die Adresse destiny_tim
Variable in diercx
Registrieren Sie sich, wouwu
erwartet den impliziten Objektparameter (aufgrund der verwendeten Aufrufkonventionen). - Der
mov
Shops42
in dieint
Mitglied vontiny_tim
. - Wir rufen dann an
uwu
Funktion. - Schließlich geben wir den zuvor auf dem Stapel zugewiesenen Speicherplatz frei und kehren zurück.
Was passiert, wenn wir stattdessen angeben uwu
seinen Objektparameter nach Wert nehmen, so?
struct just_a_little_guy {
int how_smol;
int uwu(this just_a_little_guy);
};
In diesem Fall wird der folgende Code generiert:
mov ecx, 42
jmp static int just_a_little_guy::uwu(this just_a_little_guy)
Wir ziehen einfach um 42
in das entsprechende Register und springen (jmp
) zum uwu
Funktion. Da wir keine Referenz übergeben, müssen wir nichts auf dem Stapel zuweisen. Da wir keine Zuweisung auf dem Stapel vornehmen, müssen wir die Zuweisung am Ende der Funktion nicht aufheben. Da wir die Zuordnung am Ende der Funktion nicht aufheben müssen, können wir direkt zu ihr springen uwu
anstatt dorthin zu springen und dann wieder in diese Funktion zurückzukehren, wenn sie zurückkehrt, mit call
.
Dies sind die Arten von Optimierungen, die den „Tod durch tausend Schnitte“ verhindern können, bei dem Sie immer wieder kleine Leistungseinbußen erleiden, was zu langsameren Laufzeiten führt, deren Ursache schwer zu finden ist.
SFINAE-unfreundliche Callables
Dieses Problem ist etwas esoterischer, tritt aber tatsächlich in echtem Code auf (ich weiß es, weil ich einen Fehlerbericht zu meiner erweiterten Implementierung von erhalten habe std::optional
was genau dieses Problem in der Produktion traf). Gegeben sei eine Memberfunktion von optional
genannt transform
die die angegebene Funktion nur dann für den gespeicherten Wert aufruft, wenn es einen gibt, sieht das Problem folgendermaßen aus:
struct oh_no {
void non_const();
};
tl::optional o;
o.transform([](auto&& x) { x.non_const(); }); //does not compile
Der Fehler, den MSVC hierfür ausgibt, sieht folgendermaßen aus:
Fehler C2662: „void oh_no::non_const(void)“: „dieser“ Zeiger kann nicht von „const oh_no“ in „oh_no &“ konvertiert werden.
Es wird also versucht, a zu bestehen const oh_no
als impliziter Objektparameter an non_const
, was nicht funktioniert. Aber woher kam das? const oh_no
komme aus? Die Antwort liegt in der Implementierung von optional
selbst. Hier ist eine bewusst reduzierte Version:
template
struct optional {
T t;
template
auto transform(F&& f) -> std::invoke_result_t;
template
auto transform(F&& f) const -> std::invoke_result_t;
};
Diese std::invoke_result_t
s gibt es zu machen transform
SFINAE-freundlich. Dies bedeutet im Grunde, dass Sie überprüfen können, ob ein Anruf erfolgt transform
würde kompilieren und, wenn nicht, etwas anderes tun, anstatt einfach die gesamte Kompilierung abzubrechen. Allerdings gibt es hier eine kleine Lücke in der Sprache.
Wenn Sie die Überlastungsauflösung aktivieren transform
, muss der Compiler herausfinden, welche dieser beiden Überladungen angesichts der Argumenttypen am besten übereinstimmt. Dazu müssen die Deklarationen beider instanziiert werden const
und nicht-const
Überlastungen. Wenn Sie eine aufrufbare Datei an übergeben transform
was nicht der Fall ist selbst SFINAE-freundlich und nicht gültig für a const
qualifiziertes implizites Objekt (was in meinem Beispiel der Fall ist) und dann die Deklaration des instanziiert const
Die Memberfunktion führt zu einem schwerwiegenden Compilerfehler. Uff.
Mit expliziten Objektparametern können Sie dieses Problem lösen, da die CVref-Qualifizierer dies tun abgeleitet aus dem Ausdruck, auf dem Sie die Mitgliedsfunktion aufrufen: Wenn Sie die Funktion nie auf a aufrufen const optional
dann muss der Compiler nie versuchen, diese Deklaration zu instanziieren. Gegeben std::copy_cvref_t
ab P1450:
template
struct optional {
T t;
template
auto transform(this Self&& self, F&& f)
-> std::invoke_result_t>;
};
Dadurch kann das obige Beispiel kompiliert werden, während dies weiterhin möglich ist transform
um SFINAE-freundlich zu sein.
Abschluss
Ich hoffe, dass dies dazu beigetragen hat, die Funktion und den Nutzen expliziter Objektparameter zu verdeutlichen. Sie können die Funktion in Visual Studio Version 17.2 ausprobieren. Wenn Sie Fragen, Kommentare oder Probleme mit der Funktion haben, können Sie unten einen Kommentar abgeben oder uns per E-Mail unter [email protected] oder über Twitter erreichen @VisualC.
Und Marke
C++ Developer Advocate, C++ Team