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.
Modernes C++ Mentoring
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, 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, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
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 |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
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
“`