Skip to content

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.

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

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();
}

Let Base&lt;Derived&gt; be a CRTP base, and let Derived inherit from Base&lt;Derived&gt;. When Base&lt;Derived&gt;::f() calls static_cast&lt;const Derived\&>(*this).g()The following occurs:

  1. *this has static type Base&lt;Derived&gt;. The static_cast to const Derived\& is valid because Derived inherits from Base&lt;Derived&gt; [N4950 S7.6.2.9].
  2. The call to g() on a reference of static type const Derived\& resolves to Derived::g() by ordinary overload resolution. No virtual dispatch is involved because g() is not virtual.
  3. The compiler knows the exact type Derived at the point of instantiation, so it can inline Derived::g() into Base&lt;Derived&gt;::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.

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

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& self declares an explicit object parameter. The compiler deduces Self from the type of the object on which the member function is called.
  • When p.print() is called, Self is deduced as PointSo self is const Point&.
  • When c.print() is called, Self is deduced as CircleSo self is const Circle&.
  • The base class can call self.to_string() directly — no static_cast needed.

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

AspectCRTPDeducing This (C++23)
Syntaxclass Derived : Base&lt;Derived&gt;void f(this const auto& self)
Type accessstatic_cast&lt;const Derived\&>(*this)Direct: self is already the derived type
BoilerplateHigh: each derived class repeats the template argLow: derived classes just inherit
Compile-time polyYesYes
Requires template baseYesNo
Value categoryCan only bind to lvalues (const&)Can preserve value category (auto&&``auto)
StandardC++98C++23
Heterogeneous containersNo (each Base&lt;D&gt; is a distinct type)No (each Self is a distinct type)
Can be virtualNo (the base is a template)No (template parameter deduction is static)
this pointer accessYes (standard member function)No (use self parameter instead)

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.

The deduction follows standard reference collapsing rules [N4950 S9.3.2.6]:

Self deduced asSelf&& collapses toValue category
Counter&Counter&lvalue
CounterCounter&&rvalue
const Counter&const Counter&const lvalue
const Counterconst 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.

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}