C++23: Deducing This

Anyone who thinks a small C++ standard follows a significant C++ standard is wrong. C++23 provides powerful extensions to C++20. These extensions include the core language, particularly the standard library. Today, I present a small but very impactful feature of the core language: deducing this.

Deducing this, sometimes also called explicit object parameter, allows it to make the implicit this pointer of a member function explicit. Like Python, the explicit object parameter must be the first function parameter and is called in C++, by convention, Self and self.

struct Test {
    void implicitParameter();               // implicit this pointer
    void explictParameter(this Self& self); // explicit this pointer
};

Deducing this enables new programming techniques in C++23: deduplication of function overloading based on the object’s lvalue/rvalue value category and constness. Additionally, you can reference a lambda and invoke it recursively. Furthermore, deducing this simplifies the implementation of CRTP.

Deduplicating Function Overloading

Assume you want to overload a member function based on the lvalue/rvalue value category and constness of the calling object. This means you have to overload your member function four times.

// 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';

}

Lines (1), (2), (3), and (4) are the required function overloads. Lines (1) and (2) take a non-const and const lvalue object, and lines (3) and (4) a non-const and const rvalue objects. To simplify, a lvalue is a value from which the address can be determined, and a rvalue is an only readable value. The lines (5) to (8) represent the corresponding objects. Deducing this in line (9) enables it to deduplicate the four overloads in one member function that perfectly forwards self (line 10) and calls implicitCall. This article goes into the intricacies of perfect forwarding: perfect forwarding.

Currently (July 2023), neither the brand new GCC nor Clang C++ Compiler supports this feature, but the Microsoft Visual C++ compiler partially. The following screenshot shows that the four function calls in the main function use the four different overloads of the member function implicitCall.

As I promised, deducing this is a pretty small future, but …

Reference a Lambda

The crucial idea of the Visitor Pattern is to perform operations on an object hierarchy. The object hierarchy is stable in this classical pattern, but the operations may change frequently.

The Visitor Pattern

The following program visitor.cpp exemplifies this pattern.

// 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';

}

All the car‘s components are created at the beginning of the main function. After that, the engine and the car take the carElementPrintVisitor (1 and 2). In lines (3) and (4), both objects are accepted by carElementDoVisitor. CarElement (5) and CarElementVisitor (6) are the abstract base classes of the object and operation hierarchies. The car is the most interesting component because it holds its components in a std::vector<element*> (7). The crucial observation of the visitor pattern is that it depends on two objects, which operation is performed: the visitor and the visited object.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    Here is the output of the program:

    You will find more information on the Visitor Pattern in my previous post: The Visitor Pattern. Admittedly, the Visitor Pattern is very resistant to understanding. Thanks to the overload pattern, this changes with C++23.

    The Overload Pattern

    The Overload Pattern is the modern C++ version of the Visitor Pattern. It combines variadic templates with std:variant and its function std::visit.

    Thanks to deducing this in C++23, a lambda expression can explicitly refer to its implicit lambda object.

    // 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';
        
    }
    

    The Car (1) represents the object hierarchy, and the two operations carElementPrintVisitor (2) and carElementDoVistor (3) represent the visitors. The lambda expressions in lines (4) and (5) that visit the car can reference the implicit lambda object and thus visit the concrete components of the car: car.visitCarElement(self). The output of the previous program vistitor.cpp and this program are identical.

    What’s Next?

    The Curiously Recurring Template Pattern (CRTP) is a heavily used idiom in C++. It is similarly resistant to understanding as the classic design pattern visitor. Thanks to deducing this, we can remove the C and R from the abbreviation.

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, Philipp Lenk, Hobsbawm, Charles-Jianye Chen, and Keith Jeffery.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    1 reply
    1. Mer
      Mer says:

      Very good article, but shouldn’t the running example in the “Deduplicating Function Overloading” section use the result of deducingThisCRTP?
      The actual running result on x86-64 gcc (trunk):
      “`bash
      test: non-const lvalue
      constTest: const lvalue
      std::move(test): non const rvalue
      std::move(consTest): const rvalue
      “`

      Reply

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *