Deducing This (C++23)
Explicit object parameter in C++23 — deduce the derived type in base methods, collapse ref-qualifier overloads, and write recursive lambdas cleanly.
Deducing Thissince C++23An explicit object parameter prefixed with this lets a member function deduce the full type of its own object — including cv-qualifiers, reference category, and the concrete derived type when called on a subclass — without requiring a class template parameter.
Overview
Before C++23, a member function's object was an implicit, non-deducible entity accessible only through the this pointer. That restriction forced three painful patterns: CRTP needed a base class template parameter threaded through every subclass declaration; methods returning *this by the correct value category required four near-identical ref-qualified overloads; recursive lambdas needed std::function (type-erasure overhead) or a Y-combinator.
C++23's explicit object parameter resolves all three. Writing this before the first parameter opts that function into deduction: the compiler infers the object's full type at every call site. Internally, the compiler instantiates a template — but the spelling at the call site is an ordinary method call, not a template instantiation.
One critical behavioural change: when an explicit object parameter is present, the this keyword is unavailable inside the function body. The named parameter is the sole way to access the object.
Syntax
struct S {
// Fixed type — no deduction, equivalent to a classic const-ref member
void inspect(this const S& self);
// Abbreviated template — deduces cv/ref qualifier, same object type
void mutate(this auto& self);
// Forwarding — deduces full value category; use std::forward to propagate it
auto&& chain(this auto&& self);
// Explicit template form — same semantics, enables constraints
template<typename Self>
void with_constraint(this Self&& self)
requires std::derived_from<std::remove_cvref_t<Self>, S>;
};The first parameter must carry this. Any position after that is a compile error. The function is neither static nor a classic non-static member function — it cannot be virtual, and it cannot be a coroutine.
Examples
Collapsing ref-qualifier overloads
A method returning *this by the caller's value category previously required four nearly identical overloads:
// C++20 — four overloads for one logical operation
struct Builder {
std::string data_;
Builder& set(std::string s) & { data_ = std::move(s); return *this; }
Builder&& set(std::string s) && { data_ = std::move(s); return std::move(*this); }
const Builder& set(std::string s) const& { data_ = s; return *this; }
const Builder&& set(std::string s) const&& { data_ = s; return std::move(*this); }
};
// C++23 — one template handles every cv/ref combination
struct Builder {
std::string data_;
auto&& set(this auto&& self, std::string s) {
self.data_ = std::move(s);
return std::forward<decltype(self)>(self);
}
};
Builder b;
b.set("hello"); // self deduced as Builder&
std::move(b).set("world"); // self deduced as Builder&&
const Builder cb;
cb.set("const"); // self deduced as const Builder&The pattern scales to member data access as well. The companion utility std::forward_like<Self> (covered below) propagates the value category of Self onto a member, making accessor-heavy types particularly clean.
Propagating value category with std::forward_like
std::forward_like<T>(x) is new in C++23 (<utility>). It casts x to a reference category that matches T: if T is an rvalue reference type, x is cast to an rvalue; if T is const-qualified, x gets the same const; the combination of all four cv/ref states is handled consistently. This is the right tool when a deducing-this wrapper needs to forward a member with the same value category as Self.
// C++23
#include <utility> // std::forward_like
struct Wrapper {
std::string data_;
// Returns data_ with the same cv/ref as the Wrapper itself
decltype(auto) value(this auto&& self) {
return std::forward_like<decltype(self)>(self.data_);
}
};
Wrapper w{"hello"};
auto& ref = w.value(); // Self = Wrapper& → data_ is string&
auto copy = std::move(w).value(); // Self = Wrapper&& → data_ is string&& (moved from)
const Wrapper cw{"world"};
const auto& cr = cw.value(); // Self = const Wrapper& → data_ is const string&The table below shows what std::forward_like<Self>(member) produces for each combination:
Self deduced as | forward_like<Self>(member) type |
|---|---|
Wrapper& | std::string& |
const Wrapper& | const std::string& |
Wrapper&& | std::string&& |
const Wrapper&& | const std::string&& |
Best Practice. Prefer std::forward_like<decltype(self)> over a manual cast when forwarding members inside a deducing-this function. It is correct for all four cv/ref combinations, including the rare const && case that hand-rolled logic often forgets.
CRTP without a base class template
Classic CRTP threads the derived type through the base class template parameter — easy to mistype (struct Foo : Base<Bar>) and adds a layer of boilerplate per derived class. With deducing this, the base requires no template parameter: the compiler deduces the outermost type at each call site.
// C++11–20 CRTP
template<typename Derived>
struct Comparable {
bool operator<=(const Derived& rhs) const {
return !(static_cast<const Derived&>(*this) > rhs);
}
bool operator>=(const Derived& rhs) const {
return !(static_cast<const Derived&>(*this) < rhs);
}
};
struct Point : Comparable<Point> {
int x, y;
bool operator<(const Point&) const;
bool operator>(const Point&) const;
};
// C++23 — no template parameter on the base at all
struct Comparable {
bool operator<=(this const auto& self, const auto& rhs) {
return !(self > rhs); // self has the derived type — dispatch is direct
}
bool operator>=(this const auto& self, const auto& rhs) {
return !(self < rhs);
}
};
struct Point : Comparable {
int x, y;
bool operator<(const Point&) const;
bool operator>(const Point&) const;
};
Point a{1, 2}, b{3, 4};
bool r = (a <= b); // self deduced as const Point&; calls Point::operator>Derived is never passed explicitly — the compiler deduces it from the dynamic type of the call. There is no static_cast, no Derived* arithmetic, and no risk of misnaming the template argument. The derived class declaration is simply struct Point : Comparable — one word shorter and impossible to get wrong.
A more complete migration illustrates a clone() method that must return the concrete derived type:
// C++20 — CRTP required for correct return type
template<typename Derived>
struct Cloneable {
Derived clone() const {
return static_cast<const Derived&>(*this); // copy-constructs the derived type
}
};
struct MyObj : Cloneable<MyObj> { int value; };
// C++23 — deducing this; return type is Self (the concrete type)
struct Cloneable {
auto clone(this const auto& self) {
return self; // copies self, which has the concrete derived type
}
};
struct MyObj : Cloneable { int value; };
MyObj a{42};
MyObj b = a.clone(); // returns MyObj, not CloneableBest Practice. Prefer deducing-this CRTP for dispatch mixins and per-method dispatch patterns. Stick with classic CRTP when the derived type must appear in the base's own template parameter list — for instance, when the base participates in a concept constraint or when a non-member factory function must name the derived type as a return type.
Recursive lambdas
Before C++23, writing a lambda that calls itself required either std::function (which allocates on the heap and erases the closure type) or a Y-combinator (which is opaque to most readers). The explicit object parameter eliminates both:
// C++20 — std::function wraps the lambda; heap allocation likely
std::function<int(int)> fib = [&](int n) -> int {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
};
// C++23 — self receives the closure by value; no allocation, no type erasure
auto fib = [](this auto self, int n) -> int {
return n <= 1 ? n : self(n - 1) + self(n - 2);
};
std::println("{}", fib(10)); // 55self is the closure object itself. Because a lambda has no name inside its own body, the only way to recurse is through the explicit object parameter. The return type annotation (-> int) is required here: the compiler cannot deduce a recursive return type without it.
The explicit template form unlocks generic tree traversal without naming the node type:
// C++23 — generic depth-first traversal
struct Node {
int value;
std::vector<Node> children;
};
auto depth_sum = [](this auto& self, const Node& node) -> int {
int total = node.value;
for (const auto& child : node.children)
total += self(child);
return total;
};When the closure owns mutable state, take self by reference to avoid copying the closure on every recursive call:
// C++23 — mutable closure; self by reference avoids copies of the map
auto memo_fib = [cache = std::unordered_map<int,int>{}](
this auto& self, int n) mutable -> int {
if (auto it = cache.find(n); it != cache.end())
return it->second;
if (n <= 1) return cache[n] = n;
return cache[n] = self(n - 1) + self(n - 2);
};Take self by value only when the closure is stateless (copying is free). Take self by reference when the closure holds mutable state.
Pass by value — explicit consume semantics
When self is taken by value, an lvalue call copies and an rvalue call moves — without any overloads:
struct Buffer {
std::vector<std::byte> data_;
// Rvalue call: data_ is moved into self — zero extra allocation
std::vector<std::byte> release(this Buffer self) {
return std::move(self.data_);
}
};
Buffer buf = load_file("data.bin");
auto bytes = std::move(buf).release(); // moves buf into selfConstrained mixin methods and fluent builders
Unconstrained this auto&& on a base class will instantiate for any type at all call sites. For mixin patterns, constrain the deduced type:
struct Serializable {
template<typename Self>
requires std::derived_from<std::remove_cvref_t<Self>, Serializable> // C++20 concept
std::string to_json(this Self&& self) {
return self.serialize_impl(); // self has the concrete derived type
}
};
// Abbreviated equivalent using C++20 concepts in abbreviated template syntax
struct Serializable {
std::string to_json(this std::derived_from<Serializable> auto&& self) {
return self.serialize_impl();
}
};The constraint also enables fluent builder patterns where each setter returns the concrete derived type rather than the base type. Pre-C++23, this required CRTP; with deducing this and a constraint, the base can be a plain non-template class:
// C++23 — fluent builder base; no template parameter
struct BuilderBase {
// Returns Self& so chained calls see the concrete derived type
auto& set_name(this std::derived_from<BuilderBase> auto& self, std::string name) {
self.name_ = std::move(name);
return self; // self is the derived type — chain stays concrete
}
auto& set_id(this std::derived_from<BuilderBase> auto& self, int id) {
self.id_ = id;
return self;
}
protected:
std::string name_;
int id_ = 0;
};
struct WidgetBuilder : BuilderBase {
WidgetBuilder& set_color(std::string c) {
color_ = std::move(c);
return *this;
}
Widget build() const { return {name_, id_, color_}; }
private:
std::string color_;
};
// Chain stays as WidgetBuilder& throughout — set_color is available after set_name
auto w = WidgetBuilder{}
.set_name("Button") // returns WidgetBuilder& (self deduced as WidgetBuilder)
.set_id(42) // returns WidgetBuilder&
.set_color("blue") // WidgetBuilder-specific method, still accessible
.build();Without the constraint, set_name would accept any type, and the return type would be the auto-deduced Self&, which could be anything. The std::derived_from<BuilderBase> constraint tightens this to only types that actually inherit from BuilderBase, giving both safety and correct return-type deduction.
Best Practices
- Use
this auto&&+std::forward<decltype(self)>(orstd::forward_like) for forwarding returns. It replaces all four ref-qualified overloads and propagates value category correctly across all cv/ref combinations. - Use
std::forward_like<decltype(self)>(member)when forwarding members, not plainstd::move.forward_likehandles theconst &&case that hand-written forwarding typically misses. - Constrain deduced types in base-class mixins. An unconstrained
this auto&&on a base instantiates for every unrelated type that happens to call the method through inheritance. - Name the parameter
self. It is the established community convention. Deviating from it without reason makes the code harder to scan. - Do not use the feature for simple const accessors.
int get(this const Widget& self)is noisier thanint get() const. Reserve explicit object parameters for cases where they remove genuine duplication or enable type deduction. - Prefer deducing-
thisCRTP for dispatch mixins; prefer classic CRTP when the derived type must appear in the base's template parameters (e.g., as a return type of a non-member factory or in a concept constraint on the base itself). - Annotate the return type on recursive lambdas. The compiler cannot deduce the return type of a recursive call, so
-> ReturnTypeis mandatory.
Common Pitfalls
this keyword is unavailable in the body.
The most frequent mistake when first adopting this feature is trying to use the this pointer that every other member function provides. Inside an explicit object parameter function, this simply does not exist — the compiler will reject any use of it.
struct S {
int x;
int get(this const S& self) {
return this->x; // ERROR: 'this' unavailable in explicit-object-parameter function
return self.x; // correct
}
// Returning *this is equally wrong
S& identity(this S& self) {
return *this; // ERROR
return self; // correct
}
};Virtual functions are forbidden. The language prohibits combining virtual with an explicit object parameter. There is no workaround. The reason is fundamental: virtual dispatch depends on the runtime type stored in the vtable, while an explicit object parameter is deduced from the static call-site type. The two mechanisms are incompatible.
struct Base {
virtual void process(this Base& self); // ERROR: virtual function cannot have explicit object parameter
};If you need runtime polymorphism, use a plain virtual function. If you need static dispatch to the derived type, use deducing this. They solve different problems and do not compose.
Coroutines are forbidden. A function with an explicit object parameter cannot contain co_await, co_yield, or co_return.
self must be the first parameter. Any other position is ill-formed.
void bad(int x, this auto& self); // ERROR: explicit object parameter must be firstEach unique deduced type produces a distinct instantiation. If a base-class mixin method is deduced with ten different derived types, that is ten separate template instantiations with separate object code. For hot paths, measure before assuming the generated code will inline or deduplicate.
Recursive lambdas require an explicit return type. Without the annotation, the compiler tries to deduce the return type from the body, encounters a recursive call whose return type is not yet known, and fails.
auto fib_bad = [](this auto self, int n) { // ERROR: cannot deduce return type
return n <= 1 ? n : self(n-1) + self(n-2);
};
auto fib_ok = [](this auto self, int n) -> int { // OK: return type given explicitly
return n <= 1 ? n : self(n-1) + self(n-2);
};std::forward_like is not the same as std::forward. std::forward<T>(x) propagates the reference category encoded in T; it does not change the const-qualification of x. std::forward_like<T>(x) propagates both the reference category and the const-qualification of T onto x. Mixing them up in a deducing-this accessor silently drops const-correctness in the const && case.
CRTP Simplification: Eliminating the Base Template Parameter
Classic CRTP forces a fragile coupling between base and derived that deducing this removes entirely. The derived type is encoded in the base's template argument at the declaration site — a detail the author must get right at the call site and that the compiler cannot verify independently until template instantiation.
The table below compares the two approaches across common CRTP idioms:
| Goal | Classic CRTP | Deducing this |
|---|---|---|
| Return concrete derived type | static_cast<Derived&>(*this) | self directly |
| Access derived member | static_cast<Derived*>(this)->member_ | self.member_ |
| Base declaration | template<typename Derived> struct Base | struct Base |
| Derived declaration | struct Foo : Base<Foo> | struct Foo : Base |
| Risk of misnaming | struct Foo : Base<Bar> compiles, breaks at runtime | Not possible — no argument to mistype |
A complete side-by-side migration for a printable mixin shows the code reduction:
// C++23
// Before: classic CRTP printable mixin
template<typename Derived>
struct Printable_v1 {
void print() const {
const Derived& d = static_cast<const Derived&>(*this);
std::println("{}", d.to_string());
}
};
struct Invoice_v1 : Printable_v1<Invoice_v1> {
std::string to_string() const { return "Invoice #" + std::to_string(id_); }
int id_ = 0;
};
// After: deducing this — base is a plain non-template struct
struct Printable {
void print(this const auto& self) {
std::println("{}", self.to_string()); // self IS the derived type — no cast needed
}
};
struct Invoice : Printable {
std::string to_string() const { return "Invoice #" + std::to_string(id_); }
int id_ = 0;
};
Invoice inv{42};
inv.print(); // prints: Invoice #42When the base must name the derived type outside a method body, classic CRTP is still required. For example, if the base declares a non-member friend factory function whose return type must be Derived, the template parameter cannot be avoided. Deducing this deduces only at the method call site; it cannot propagate the derived type to the class's own non-member declarations.
Recursive Lambdas: No std::function, No Y-Combinator
Recursive lambdas before C++23 paid a real cost: std::function erases the closure type and typically causes a heap allocation, adding per-call indirection on every recursive step. The explicit object parameter eliminates both costs by giving the lambda a name for itself inside its own body, letting the compiler inline the recursive call like any other template.
The pattern generalises beyond arithmetic. Any algorithm that would normally be written as a named free function can instead be expressed as a self-contained lambda, making it easier to capture local state without threading extra parameters:
// C++23
#include <vector>
#include <algorithm>
// Iterative depth computation — captures nothing, stateless closure
auto tree_depth = [](this auto self, const Node& n) -> int {
if (n.children.empty()) return 0;
int max_child = 0;
for (const auto& c : n.children)
max_child = std::max(max_child, self(c));
return 1 + max_child;
};
// Accumulating visitor — captures a reference to an output vector
std::vector<int> leaves;
auto collect_leaves = [&leaves](this auto& self, const Node& n) -> void {
if (n.children.empty()) { leaves.push_back(n.value); return; }
for (const auto& c : n.children) self(c);
};Mutual recursion between two lambdas requires a small workaround because each lambda must refer to the other before it is named. A struct holding both works cleanly:
// C++23 — mutual recursion via an aggregate holding both lambdas
struct IsEvenOdd {
auto is_even(this const IsEvenOdd& self, int n) -> bool {
return n == 0 || self.is_odd(n - 1);
}
auto is_odd(this const IsEvenOdd& self, int n) -> bool {
return n != 0 && self.is_even(n - 1);
}
};
IsEvenOdd checker;
checker.is_even(4); // true
checker.is_odd(7); // truePass self by value for stateless closures (copying is free) and by reference for closures that own mutable state — the same rule applies here as for memoised variants covered in the Examples section above.
Builder Pattern: Fluent Chaining That Preserves the Derived Type
The builder pattern's chief frustration in C++ has always been that base-class setters return Base&, severing access to derived-class methods mid-chain. Pre-C++23, solving this without CRTP meant either duplicating every setter in each derived builder or accepting the broken chain. Deducing this makes the fix one-word: this auto&.
A production-grade builder often needs to support both lvalue chains (accumulating into a named builder) and rvalue chains (temporary builder, move-optimised). With classic overloads that is eight overloads for four setters; with deducing this it is four:
// C++23
struct QueryBuilder {
// Each setter returns Self& (lvalue) or Self&& (rvalue) automatically
auto&& from(this auto&& self, std::string table) {
self.table_ = std::move(table);
return std::forward<decltype(self)>(self);
}
auto&& where(this auto&& self, std::string condition) {
self.conditions_.push_back(std::move(condition));
return std::forward<decltype(self)>(self);
}
auto&& limit(this auto&& self, int n) {
self.limit_ = n;
return std::forward<decltype(self)>(self);
}
protected:
std::string table_;
std::vector<std::string> conditions_;
int limit_ = 0;
};
struct JoinQueryBuilder : QueryBuilder {
auto&& join(this auto&& self, std::string other) {
self.join_table_ = std::move(other);
return std::forward<decltype(self)>(self);
}
std::string build() const;
private:
std::string join_table_;
};
// Chain never loses the JoinQueryBuilder type — .join() is reachable anywhere
auto q = JoinQueryBuilder{}
.from("orders") // self deduced as JoinQueryBuilder&&
.join("customers") // still JoinQueryBuilder&&
.where("id > 100") // still JoinQueryBuilder&&
.limit(50) // still JoinQueryBuilder&&
.build();std::forward<decltype(self)>(self) propagates the exact value category of the incoming self. An lvalue JoinQueryBuilder call produces JoinQueryBuilder&; an rvalue produces JoinQueryBuilder&&. Both paths share the same function body — no duplication, no manual cast.
Constrain the deduced type when the base is part of a public API. An unconstrained this auto&& on a widely included header will instantiate for any accidental type that inherits from the builder, including types the author did not anticipate. Adding requires std::derived_from<std::remove_cvref_t<decltype(self)>, QueryBuilder> (or the abbreviated concept form) keeps instantiation scoped to genuine subclasses and surfaces misuse as a cleaner diagnostic.