C++23: Deducing This erstellt explizite Zeiger
Mit der C++-Neuerung Deducing This, die auch expliziter Objektparameter heiĂźt, wird der implizite this-Zeiger einer Mitgliedsfunktion explizit.
- Rainer Grimm
Wer denkt, dass einem bedeutenden C++-Standard nur ein kleiner folgt, liegt falsch. C++23 bietet mächtige Erweiterungen zu C++20 an. Diese Erweiterungen umfassen die Kernsprache und vor allem die Standardbibliothek. Heute stelle ich ein kleines, aber sehr einflussreiches Feature der Kernsprache vor: Deducing This.
Deducing This, manchmal auch expliziter Objektparameter genannt, ermöglicht es, den impliziten this
-Zeiger einer Mitgliedsfunktion explizit zu machen. Ă„hnlich wie in Python muss der explizite Objektparameter der erste Funktionsparameter sein und er wird per Konvention Self
und self
genannt. Zum jetzigen Zeitpunkt unterstĂĽtzt lediglich ein aktueller Windows-Compiler Deducing This.
struct Test {
void implicitParameter(); // implicit this pointer
void explictParameter(this Self& self); // explicit this pointer
};
Diese neue Programmiertechnik in C++23 ermöglicht das Deduplizieren der Funktionsüberladung basierend auf der lvalue/rvalue-Wertekategorie des Objekts und seiner Konstantheit. Dank Deducing This lässt sich ein Lambda referenzieren oder CRTP (Curiously Recurring Template Pattern) deutlich einfacher implementieren.
Deduplikation der FunktionsĂĽberladung
Angenommen, eine Mitgliedsfunktion soll basierend auf der lvalue/rvalue-Wertkategorie und der Konstantheit des aufrufenden Objekts ĂĽberladen werden. Das bedeutet viel Schreibarbeit. Die Mitgliedsfunktion muss viermal ĂĽberladen werden.
// deducingThis.cpp
#include <iostream>
struct Test {
template <typename Self>
void explicitCall(this Self&& self,
const std::string& text) { // (9)
std::cout << text << ": ";
std::forward<Self>(self).implicitCall(); // (10)
std::cout << '\n';
}
void implicitCall() & { // (1)
std::cout << "non const lvalue";
}
void implicitCall() const& { // (2)
std::cout << "const lvalue";
}
void implicitCall() && { // (3)
std::cout << "non const rvalue";
}
void implicitCall() const&& { // (4)
std::cout << "const rvalue";
}
};
int main() {
std::cout << '\n';
Test test;
const Test constTest;
test.explicitCall("test"); // (5)
constTest.explicitCall("constTest"); // (6)
std::move(test).explicitCall("std::move(test)"); // (7)
std::move(constTest).explicitCall("std::move(consTest)"); // (8)
std::cout << '\n';
}
Die Zeilen (1), (2), (3) und (4) sind die erforderlichen Funktionsüberladungen. (1) und (2) nehmen einen nicht konstanten und einen konstanten Lvalue, (3) und (4) einen nicht konstanten und konstanten Rvalue an. Vereinfachend erklärt, ist ein Lvalue ein Wert, von dem sich die Adresse bestimmen lässt und ein Rvalue ein temporärer Wert. (5) bis (8) sind die entsprechenden Objekte. this
(9) ermöglicht es, die vier Überladungen in einer Mitgliedsfunktion zu deduplizieren, die self
(10) perfekt weiterleitet (perfect forwarding) und implicitCall
aufruft. Dieser Artikel geht auf die Feinheiten von Perfekt Forwarding ein: Perfekt Forwarding. Der folgende Screenshot zeigt schön, dass die vier Funktionsaufrufe in der main-Funktion die vier verschiedenen Überladungen der Funktion implicitCall
verwenden.
Zugegeben, das vorgestellte Beispiel war sehr akademisch. Dies ändert sich aber nun.
Eine Lambda-Funktion referenzieren
Die entscheidende Idee des Besuchermusters ist es, Operationen auf einer Objekthierarchie durchzuführen. Die Objekthierarchie ist in diesem klassischen Muster stabil, aber die Operationen können sich häufig ändern.
Das Besuchermuster
Das folgende Programm visitor.cpp
zeigt das Besuchermuster in Aktion.
// visitor.cpp
#include <iostream>
#include <string>
#include <vector>
class CarElementVisitor;
class CarElement { // (5)
public:
virtual void accept(CarElementVisitor& visitor) const = 0;
virtual ~CarElement() = default;
};
class Body;
class Car;
class Engine;
class Wheel;
class CarElementVisitor { // (6)
public:
virtual void visit(Body body) const = 0;
virtual void visit(Car car) const = 0;
virtual void visit(Engine engine) const = 0;
virtual void visit(Wheel wheel) const = 0;
virtual ~CarElementVisitor() = default;
};
class Wheel: public CarElement {
public:
Wheel(const std::string& n): name(n) { }
void accept(CarElementVisitor& visitor) const override {
visitor.visit(*this);
}
std::string getName() const {
return name;
}
private:
std::string name;
};
class Body: public CarElement {
public:
void accept(CarElementVisitor& visitor) const override {
visitor.visit(*this);
}
};
class Engine: public CarElement {
public:
void accept(CarElementVisitor& visitor) const override {
visitor.visit(*this);
}
};
class Car: public CarElement {
public:
Car(std::initializer_list<CarElement*> carElements ):
elements{carElements} {}
void accept(CarElementVisitor& visitor) const override {
for (auto elem : elements) {
elem->accept(visitor);
}
visitor.visit(*this);
}
private:
std::vector<CarElement*> elements; // (7)
};
class CarElementDoVisitor: public CarElementVisitor {
void visit(Body body) const override {
std::cout << "Moving my body" << '\n';
}
void visit(Car car) const override {
std::cout << "Starting my car" << '\n';
}
void visit(Wheel wheel) const override {
std::cout << "Kicking my " << wheel.getName()
<< " wheel" << '\n';
}
void visit(Engine engine) const override {
std::cout << "Starting my engine" << '\n';
}
};
class CarElementPrintVisitor: public CarElementVisitor {
void visit(Body body) const override {
std::cout << "Visiting body" << '\n';
}
void visit(Car car) const override {
std::cout << "Visiting car" << '\n';
}
void visit(Wheel wheel) const override {
std::cout << "Visiting " << wheel.getName()
<< " wheel" << '\n';
}
void visit(Engine engine) const override {
std::cout << "Visiting engine" << '\n';
}
};
int main() {
std::cout << '\n';
Wheel wheelFrontLeft("front left");
Wheel wheelFrontRight("front right");
Wheel wheelBackLeft("back left");
Wheel wheelBackRight("back right");
Body body;
Engine engine;
Car car {&wheelFrontLeft, &wheelFrontRight,
&wheelBackLeft, &wheelBackRight,
&body, &engine};
CarElementPrintVisitor carElementPrintVisitor;
engine.accept(carElementPrintVisitor); // (1)
car.accept(carElementPrintVisitor); // (2)
std::cout << '\n';
CarElementDoVisitor carElementDoVisitor;
engine.accept(carElementDoVisitor); // (3)
car.accept(carElementDoVisitor); // (4)
std::cout << '\n';
}
Zu Beginn der main
-Funktion werden alle Bestandteile des Autos erstellt. Danach nehmen die engine
und das car
den carElementPrintVisitor
an (1) und (2). In (3) und (4) werden beide Objekte von carElementDoVisitor
angenommen. CarElement
(5) und CarElementVisitor
(6) sind die abstrakten Basisklassen der Objekthierarchie und der Operationshierarchie. Das Auto ist die interessanteste Komponente, denn es hält seine Komponenten in einem std::vector<Element*>
(7). Die entscheidende Beobachtung des Besuchermusters ist, dass es von zwei Objekten abhängt, welche Operation durchgeführt wird: dem Besucher und dem besuchten Objekt.
Der folgende Screenshot zeigt die Ausgabe des Programms:
Mehr Information zu diesem Muster bietet der Artikel Patterns in der Softwareentwicklung: Das Besuchermuster. Zugegeben, das Besuchermuster ist sehr Verständnis-resistent. Dies ändert sich mit C++23 dank des Overload Patterns.
Overload Pattern
Das Overload Pattern ist die moderne C++-Version des Besuchermusters. Es kombiniert Variadic Templates mit std:variant
und seiner Funktion std::visit
. Dank Deducing This in C++23 kann ein Lambda-Ausdruck explizit sein implizites Lambda-Objekt verwenden.
// visitOverload.cpp
#include <iostream>
#include <string>
#include <vector>
#include <variant>
template<class... Ts> struct overloaded : Ts... {
using Ts::operator()...;
};
class Wheel {
public:
Wheel(const std::string& n): name(n) { }
std::string getName() const {
return name;
}
private:
std::string name;
};
class Body {};
class Engine {};
class Car;
using CarElement = std::variant<Wheel, Body, Engine, Car>;
class Car {
public:
Car(std::initializer_list<CarElement*> carElements ):
elements{carElements} {}
template<typename T>
void visitCarElements(T&& visitor) const {
for (auto elem : elements) {
std::visit(visitor, *elem);
}
}
private:
std::vector<CarElement*> elements;
};
overloaded carElementPrintVisitor { // (2)
[](const Body& body)
{ std::cout << "Visiting body" << '\n'; },
[](this auto const& self, const Car& car)
{ car.visitCarElements(self); // (4)
std::cout << "Visiting car" << '\n'; },
[](const Wheel& wheel)
{ std::cout << "Visiting "
<< wheel.getName() << " wheel" << '\n'; },
[](const Engine& engine)
{ std::cout << "Visiting engine" << '\n';}
};
overloaded carElementDoVisitor { // (3)
[](const Body& body)
{ std::cout << "Moving my body" << '\n'; },
[](this auto const& self, const Car& car)
{ car.visitCarElements(self); // (5)
std::cout << "Starting my car" << '\n'; },
[](const Wheel& wheel)
{ std::cout << "Kicking my "
<< wheel.getName() << " wheel" << '\n'; },
[](const Engine& engine)
{ std::cout << "Starting my engine" << '\n';}
};
int main() {
std::cout << '\n';
CarElement wheelFrontLeft = Wheel("front left");
CarElement wheelFrontRight = Wheel("front right");
CarElement wheelBackLeft = Wheel("back left");
CarElement wheelBackRight = Wheel("back right");
CarElement body = Body{};
CarElement engine = Engine{};
CarElement car = Car{&wheelFrontLeft, &wheelFrontRight, // (1)
&wheelBackLeft, &wheelBackRight,
&body, &engine};
std::visit(carElementPrintVisitor, engine);
std::visit(carElementPrintVisitor, car);
std::cout << '\n';
std::visit(carElementDoVisitor, engine);
std::visit(carElementDoVisitor, car);
std::cout << '\n';
}
Car
(1) steht fĂĽr die Objekthierarchie, und die beiden Operationen carElementPrintVisitor
(2) und carElementDoVistor
(3) fĂĽr die Besucher. Die Lambda-AusdrĂĽcke in (4) und (5), die Car
besuchen, können das implizite Lambda-Objekt referenzieren und damit die konkreten Komponenten des Autos besuchen: car.visitCarElement(self)
.
Die Ausgaben des visitor.cpp
und visitOverload.cpp
sind identisch:
Wie geht's weiter?
Das Curiously Recurring Template Pattern (CRTP) ist ein häufig verwendetes Idiom in C++. Es ist ähnlich schwer zu verstehen wie das klassische Design Pattern Visitor. Dank Deducing This können wir das C und R aus der Abkürzung in C++23 entfernen. (rme)