Deducing this — Explicit Object Parameter (C++23)
Every non-static member function in C++ has an implicit this pointer — a hidden parameter whose type and value category are determined by the function's cv-qualifiers and ref-qualifiers. C++23 makes that parameter explicit: you can write it as the first parameter of a member function, prefixed with the this keyword. When the explicit object parameter's type is itself a template type parameter, the compiler deduces the cv-qualifiers and value category from the call context — a feature called deducing this. The main payoff is eliminating the duplication of const and non-const member function overloads that have identical bodies.
Basic syntax — making this explicit
To use an explicit object parameter, write this Type& self (or any name you like) as the first parameter. The this keyword here is a modifier on the parameter, not a pointer expression. Once a member function has an explicit object parameter, the implicit this pointer is gone: inside the body you must use self (or whatever name you chose) to access data members and other member functions. The call site is completely unchanged — you still call obj.method(arg), not obj.method(obj, arg).
Implicit this (classic)
class SpreadsheetCell
{
public:
void setValue(double value)
{
// implicit this pointer
this->m_value = value;
}
private:
double m_value;
};
SpreadsheetCell c;
c.setValue(6.0);Explicit object parameter (C++23)
class SpreadsheetCell
{
public:
void setValue(this SpreadsheetCell& self,
double value)
{
// self replaces this; no implicit this
self.m_value = value;
}
private:
double m_value;
};
SpreadsheetCell c;
c.setValue(6.0); // call site unchangedIn this basic form the explicit object parameter adds verbosity without benefit. Its value emerges in three specific scenarios covered below.
The problem: duplicated const/non-const overloads
A function that returns a reference to a member must provide both a const and a non-const overload so that callers can read through a const object and write through a mutable one. When the body is non-trivial — involving bounds checking, coordinate validation, or complex traversal — writing it twice is error-prone. The classic workaround is Scott Meyers' const_cast pattern, but that requires careful reasoning about casts and is still two definitions.
// C++17 approach: two overloads, const_cast to avoid duplication
template <typename T>
class Grid
{
public:
// const version — the real implementation
const std::optional<T>& at(std::size_t x, std::size_t y) const
{
verifyCoordinate(x, y);
return m_cells[x + y * m_width];
}
// non-const version — just casts away const from the result of the const version
std::optional<T>& at(std::size_t x, std::size_t y)
{
return const_cast<std::optional<T>&>(std::as_const(*this).at(x, y));
}
// ...
};Deducing this with a template parameter — one definition for all
When the explicit object parameter's type is a template type parameter of the member function itself (not of the class), the compiler deduces that parameter from the call context. If called on a const Grid<T>, Self is deduced as const Grid<T>; on a mutable one, as Grid<T>. Writing Self&& creates a forwarding reference that binds to all three cases: lvalue, const lvalue, and rvalue.
// C++23: single member function template covers const and non-const
template <typename T>
class Grid
{
public:
template <typename Self>
auto&& at(this Self&& self, std::size_t x, std::size_t y)
{
// 'this' pointer is gone; use 'self' everywhere
self.verifyCoordinate(x, y);
return std::forward_like<Self>(self.m_cells[x + y * self.m_width]);
}
// ...
};std::forward_like<Self>(x) (C++23, <utility>) returns a reference to x with the same value category and constness as Self&&. This is necessary because self.m_cells[...] yields an optional<T>& regardless of whether self is const. The forward_like call propagates the right constness automatically.
| Call context | Self deduced as | Return type |
|---|---|---|
| grid.at(x, y) on mutable Grid<T> | Grid<T> | optional<T>& |
| grid.at(x, y) on const Grid<T> | const Grid<T> | const optional<T>& |
| std::move(grid).at(x, y) | Grid<T>&& | optional<T>&& |
Self&& is a forwarding reference only in member function templates
A subtle but important rule: Self&& is a forwarding reference only when Self is a template type parameter of the member function itself, not of the class. If the class template already has a parameter T and you write this T&& self without introducing a fresh Self parameter on the function, then by the time the compiler processes the function, T is already a concrete type (e.g., int) and T&& is an ordinary rvalue reference, not a forwarding reference.
template <typename T>
class Grid {
public:
// WRONG: T is already resolved; T&& is NOT a forwarding reference here
auto&& at(this T&& self, size_t x, size_t y);
// CORRECT: Self is a new template param on the function itself
template <typename Self>
auto&& at(this Self&& self, size_t x, size_t y);
};Ref-qualified member functions — a cleaner syntax
Before C++23, member functions could be ref-qualified with & or && suffixes to restrict calls to lvalues or rvalues. The explicit object parameter expresses the same constraint without special suffix syntax: just declare the parameter type as an lvalue or rvalue reference. This is more consistent with ordinary function overloading.
class Buffer
{
public:
// C++17 ref-qualified syntax
std::string& data() & { return m_data; } // called on lvalue
std::string data() && { return std::move(m_data); } // called on rvalue
// C++23 explicit object parameter syntax — same semantics, consistent syntax
std::string& data(this Buffer& self) { return self.m_data; }
std::string data(this Buffer&& self) { return std::move(self.m_data); }
private:
std::string m_data;
};Recursive lambdas
A lambda cannot refer to itself by name — there is no name in scope during its body. The explicit object parameter solves this: the first parameter receives the lambda itself, so the body can call it recursively without capturing or naming the lambda externally. This enables elegant tree traversal or combinatorial algorithms that previously required either a named function or the std::function wrapper (which carries virtual dispatch overhead).
// Factorial with a recursive lambda — no std::function overhead
auto factorial = [](this auto self, int n) -> int {
return n <= 1 ? 1 : n * self(n - 1);
};
std::println("{}", factorial(10)); // 3628800
// Fibonacci — same pattern
auto fib = [](this auto self, int n) -> int {
if (n <= 1) return n;
return self(n - 1) + self(n - 2);
};
// Recursive tree traversal
struct Node { int value; std::vector<Node> children; };
auto sum_tree = [](this auto self, const Node& node) -> int {
int total = node.value;
for (const auto& child : node.children)
total += self(child);
return total;
};When to reach for deducing this
You have const and non-const overloads with identical bodies
Write a single member function template with this Self&& self + std::forward_like<Self>You want ref-qualified members in a style consistent with normal overloading
Replace the & / && suffix with this T& self / this T&& self parameterYou need a recursive lambda without std::function overhead
Write the first parameter as this auto self and call it recursivelySimple member functions with no const/overload or recursion needs
Stick with implicit this — explicit object parameter adds noise without benefit