Deducing this
C++23 introduces the explicit object parameter, allowing type deduction of the this parameter to simplify CRTP, eliminate redundant reference-qualified overloads, and support recursive lambdas.
Basic Syntax
cpp
struct Widget {
// Traditional: const/non-const requires two overloads
void greet() const { std::cout << "const\n"; }
void greet() { std::cout << "non-const\n"; }
// C++23: merged into a single template function
template <typename Self>
void greet(this Self&& self) {
if constexpr (std::is_const_v<std::remove_reference_t<Self>>)
std::cout << "const\n";
else
std::cout << "non-const\n";
}
};this Self&& self is the explicit object parameter; the compiler passes the object as the first explicit argument.
Eliminating Redundant Reference-Qualified Overloads
cpp
// Traditional — 4 overloads, repeated logic
struct Container {
std::string& name() & { return name_; }
const std::string& name() const& { return name_; }
std::string name() && { return std::move(name_); }
};
// C++23 — single template function
struct Container {
template <typename Self>
auto&& name(this Self&& self) {
return std::forward<Self>(self).name_;
}
};CRTP Alternative
cpp
// Traditional CRTP — verbose
template <typename Derived>
struct Base {
void interface() { static_cast<Derived*>(this)->implementation(); }
};
struct MyClass : Base<MyClass> {
void implementation() { /* ... */ }
};
// C++23 — concise
struct Base {
template <typename Self>
void interface(this Self&& self) { self.implementation(); }
};
struct MyClass : Base {
void implementation() { /* ... */ }
};Practical Application: Polymorphic Copy
cpp
struct Shape {
virtual ~Shape() = default;
template <typename Self>
auto clone(this Self&& self) {
return std::make_unique<std::decay_t<Self>>(std::forward<Self>(self));
}
};
struct Circle : Shape {
double radius;
Circle(double r) : radius(r) {}
};
auto c = std::make_unique<Circle>(5.0);
auto c2 = c->clone(); // unique_ptr<Circle>Recursive Lambdas
Before C++23, lambdas could not directly call themselves recursively. The explicit object parameter solves this:
cpp
// C++23 recursive lambda — zero overhead
auto factorial = [](this auto self, int n) -> int {
return n <= 1 ? 1 : n * self(n - 1);
};
std::cout << factorial(5) << "\n"; // 120
// Recursive tree traversal
auto traverse = [](this auto self, const Tree* node) -> void {
if (!node) return;
self(node->left);
std::cout << node->value << " ";
self(node->right);
};The traditional approach requires std::function (with indirect call overhead) or a Y-combinator (complex). The C++23 approach lets the compiler inline the recursive calls directly.
Value Category Deduction
cpp
struct StringWrapper {
std::string data;
template <typename Self>
auto&& get(this Self&& self) {
return std::forward<Self>(self).data;
}
};
StringWrapper w{"hello"};
auto& s = w.get(); // string&
auto&& s2 = std::move(w).get(); // string&&Cache-Friendly Memoization
cpp
auto fib_memo = [](this auto self, int n,
std::unordered_map<int, int>& cache) -> int {
if (n <= 1) return n;
if (auto it = cache.find(n); it != cache.end()) return it->second;
int result = self(n - 1, cache) + self(n - 2, cache);
return cache[n] = result;
};Caveats
- The explicit object parameter cannot coexist with a traditional implicit
thisin the same member function - Cannot be used with constructors or destructors (
operator()in lambdas is fine) - Virtual functions can use it, but
Selfdeduction is based on the static type - The
thisparameter must be the function's first parameter