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.

In Pocket speichern vorlesen Druckansicht 6 Kommentare lesen

(Bild: Piyawat Nandeenopparit / Shutterstock.com)

Lesezeit: 4 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

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.

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.

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 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.

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:

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)