Deducing This and CRTP
Explicit Object Parameters (Deducing This) and CRTP Replacement
Section titled “Explicit Object Parameters (Deducing This) and CRTP Replacement”C++23 introduces explicit object parameters (deducing this), which eliminates the need for the Curiously Recurring Template Pattern (CRTP) in most cases. This section covers the CRTP pattern, the New this parameter syntax, value category preservation, and practical patterns for fluent APIs and Mixin classes.
5.1 The CRTP Pattern
Section titled “5.1 The CRTP Pattern”The Curiously Recurring Template Pattern (CRTP) is a compile-time technique where a derived Class passes itself as a template parameter to its base class. It enables static polymorphism — the Base class can call methods on the derived class without virtual dispatch [N4950 S13.3.3].
Formal Definition
Section titled “Formal Definition”CRTP is defined as follows: given a class template Base<Derived>A derived class Derived Inherits from Base<Derived>. The base class template can static_cast<const Derived\&>(*this) to access the derived class”s interface. Because Derived Is a template parameter, the cast is resolved at compile time, and the call to Derived’s method is A direct call (not a virtual dispatch).
#include <iostream>#include <sstream>
template <typename Derived>struct Serializable { std::string serialize() const { std::ostringstream oss; oss << static_cast<const Derived&>(*this).to_string(); return oss.str(); }
void print_serialized() const { std::cout << serialize() << "\n"; }};
struct Point : Serializable<Point> { double x; double y;
Point(double x, double y) : x(x), y(y) {}
std::string to_string() const { return "Point(" + std::to_string(x) + ", " + std::to_string(y) + ")"; }};
struct Circle : Serializable<Circle> { double cx; double cy; double radius;
Circle(double cx, double cy, double r) : cx(cx), cy(cy), radius(r) {}
std::string to_string() const { return "Circle(" + std::to_string(cx) + ", " + std::to_string(cy) + ", r=" + std::to_string(radius) + ")"; }};
int main() { Point p{1.0, 2.0}; Circle c{3.0, 4.0, 5.0};
p.print_serialized(); c.print_serialized();}Proof: CRTP Achieves Static Dispatch
Section titled “Proof: CRTP Achieves Static Dispatch”Let Base<Derived> be a CRTP base, and let Derived inherit from Base<Derived>. When Base<Derived>::f() calls static_cast<const Derived\&>(*this).g()The following occurs:
*thishas static typeBase<Derived>. Thestatic_casttoconst Derived\&is valid becauseDerivedinherits fromBase<Derived>[N4950 S7.6.2.9].- The call to
g()on a reference of static typeconst Derived\&resolves toDerived::g()by ordinary overload resolution. No virtual dispatch is involved becauseg()is not virtual. - The compiler knows the exact type
Derivedat the point of instantiation, so it can inlineDerived::g()intoBase<Derived>::f().
Therefore, CRTP achieves static dispatch with zero runtime overhead (no vtable lookup, no Indirection). The cost is compile-time instantiation of the template for each derived type.
Limitations of CRTP
Section titled “Limitations of CRTP”- Verbose boilerplate: every derived class must repeat the base class template parameter.
- The
static_cast<const Derived&>(*this)pattern is unintuitive and error-prone. - Does not work with type erasure or heterogeneous containers (all types must be known at compile time).
- Cannot be used when the derived type is not known at the point of base class definition.
- Cannot distinguish between lvalue and rvalue receivers (see section 5.4).
5.2 C++23 Deducing This
Section titled “5.2 C++23 Deducing This”C++23 introduces explicit object parameters (also called “deducing this”) [N4950 S11.4.8.3]. A Member function can declare its object parameter explicitly using the this keyword in the Parameter list:
#include <iostream>#include <sstream>#include <string>
struct Printable { template <typename Self> void print(this const Self& self) { std::cout << self.to_string() << "\n"; }};
struct Point : Printable { double x; double y;
Point(double x, double y) : x(x), y(y) {}
std::string to_string() const { return "Point(" + std::to_string(x) + ", " + std::to_string(y) + ")"; }};
struct Circle : Printable { double cx; double cy; double radius;
Circle(double cx, double cy, double r) : cx(cx), cy(cy), radius(r) {}
std::string to_string() const { return "Circle(" + std::to_string(cx) + ", " + std::to_string(cy) + ", r=" + std::to_string(radius) + ")"; }};
int main() { Point p{1.0, 2.0}; Circle c{3.0, 4.0, 5.0};
p.print(); c.print();}How it works:
- The syntax
this const Self& selfdeclares an explicit object parameter. The compiler deducesSelffrom the type of the object on which the member function is called. - When
p.print()is called,Selfis deduced asPointSoselfisconst Point&. - When
c.print()is called,Selfis deduced asCircleSoselfisconst Circle&. - The base class can call
self.to_string()directly — nostatic_castneeded.
Formal Semantics [N4950 S11.4.8.3]
Section titled “Formal Semantics [N4950 S11.4.8.3]”An explicit object member function is a member function whose first parameter is a deduced type Parameter with a placeholder type that includes the this keyword. The this keyword in the Parameter list serves as a marker that the parameter represents the object on which the member Function is invoked.
The transformation is equivalent to a non-member function where the first parameter is the object:
struct S { void f(this const Self& self); // Equivalent to: template <typename Self> void f(const Self& self);};When s.f() is called, the compiler performs template argument deduction on the first parameter, Deducing Self as the type of s (with cv-qualifiers). The call s.f(args) is transformed into f(s, args).
5.3 CRTP vs Deducing This: Comparison
Section titled “5.3 CRTP vs Deducing This: Comparison”| Aspect | CRTP | Deducing This (C++23) |
|---|---|---|
| Syntax | class Derived : Base<Derived> | void f(this const auto& self) |
| Type access | static_cast<const Derived\&>(*this) | Direct: self is already the derived type |
| Boilerplate | High: each derived class repeats the template arg | Low: derived classes just inherit |
| Compile-time poly | Yes | Yes |
| Requires template base | Yes | No |
| Value category | Can only bind to lvalues (const&) | Can preserve value category (auto&&``auto) |
| Standard | C++98 | C++23 |
| Heterogeneous containers | No (each Base<D> is a distinct type) | No (each Self is a distinct type) |
| Can be virtual | No (the base is a template) | No (template parameter deduction is static) |
this pointer access | Yes (standard member function) | No (use self parameter instead) |
5.4 Value Category Preservation
Section titled “5.4 Value Category Preservation”A key advantage of deducing this over CRTP is the ability to preserve the value category of the Object:
#include <utility>#include <iostream>
struct Counter { int count = 0;
template <typename Self> auto&& increment(this Self&& self) { ++self.count; return std::forward<Self>(self); }};
int main() { Counter c1; Counter c2;
c1.increment().increment().increment(); std::cout << "c1.count = " << c1.count << "\n";
std::move(c2).increment(); std::cout << "c2.count = " << c2.count << "\n";}The Self&& parameter deduces to:
Counter&when called on an lvalue (preserving the lvalue reference).Counter&&when called on an rvalue (preserving the rvalue reference, enabling move semantics).
This is impossible with CRTP, which can only bind to const Derived& or Derived& — it cannot Distinguish between lvalue and rvalue receivers.
Reference Collapsing Rules
Section titled “Reference Collapsing Rules”The deduction follows standard reference collapsing rules [N4950 S9.3.2.6]:
Self deduced as | Self&& collapses to | Value category |
|---|---|---|
Counter& | Counter& | lvalue |
Counter | Counter&& | rvalue |
const Counter& | const Counter& | const lvalue |
const Counter | const Counter&& | const rvalue |
5.5 Deducing This for a Fluent API Builder
Section titled “5.5 Deducing This for a Fluent API Builder”Deducing this enables fluent builder patterns where the return type adapts to the most-derived Class:
#include <iostream>#include <string>
struct BuilderBase { template <typename Self> Self& set_name(this Self& self, std::string name) { self.name_ = std::move(name); return self; }
protected: std::string name_;};
struct HttpConfig : BuilderBase { template <typename Self> Self& set_port(this Self& self, int port) { self.port_ = port; return self; }
template <typename Self> Self& set_timeout(this Self& self, int timeout_ms) { self.timeout_ms_ = timeout_ms; return self; }
void display() const { std::cout << "Server: " << name_ << ":" << port_ << " (timeout: " << timeout_ms_ << "ms)\n"; }
private: int port_ = 80; int timeout_ms_ = 30000;};
struct GrpcConfig : BuilderBase { template <typename Self> Self& set_max_retries(this Self& self, int retries) { self.max_retries_ = retries; return self; }
void display() const { std::cout << "Service: " << name_ << " (max retries: " << max_retries_ << ")\n"; }
private: int max_retries_ = 3;};
int main() { HttpConfig http; http.set_name("api.example.com") .set_port(443) .set_timeout(5000) .display();
GrpcConfig grpc; grpc.set_name("order-service") .set_max_retries(5) .display();}Without deducing this, each builder method in a base class would return BuilderBase&Breaking the Chain when the derived class adds its own methods. CRTP solves this but with significant Boilerplate. Deducing this solves it with minimal syntax.
5.6 Deducing This for Mixin Classes
Section titled “5.6 Deducing This for Mixin Classes”Deducing this makes mixin classes straightforward — a mixin can provide methods that return the Correct derived type without requiring CRTP:
#include <iostream>#include <string>#include <sstream>
struct JsonMixin { template <typename Self> std::string to_json(this const Self& self) { std::ostringstream oss; oss << "{"; bool first = true; self.visit_fields([&](const char* name, auto value) { if (!first) oss << ", "; first = false; if constexpr (std::is_convertible_v<decltype(value), std::string>) { oss << "\"" << name << "\":\"" << value << "\""; } else { oss << "\"" << name << "\":" << value; } }); oss << "}"; return oss.str(); }};
struct Person : JsonMixin { std::string name; int age;
Person(std::string n, int a) : name(std::move(n)), age(a) {}
template <typename F> void visit_fields(F&& f) const { f("name", name); f("age", age); }};
struct Product : JsonMixin { std::string title; double price;
Product(std::string t, double p) : title(std::move(t)), price(p) {}
template <typename F> void visit_fields(F&& f) const { f("title", title); f("price", price); }};
int main() { Person alice{"Alice", 30}; Product widget{"Widget", 9.99};
std::cout << alice.to_json() << "\n"; std::cout << widget.to_json() << "\n";}Output:
{"name":"Alice","age":30}{"title":"Widget","price":9.99}