🔴 🟡 🟢Deducing this in C++23
← Back to Posts

Deducing this in C++23

Deducing this

Python fans will love this one!

C++23 introduces the explicit object parameter, which allows the implicit this pointer in member functions to be made explicit. This is colloquially known as "deducing this".

Background & Motivation

Since C++03, member functions can have cv-qualifiers (const or volatile). It's common to see overloading based on these qualifiers. And since C++11, we can overload based on reference qualifiers. But that's quite verbose and not elegant! We'd have to rewrite the same logic many times, or delegate all methods to a separate static helper. Oftentimes, the only difference is in the types being accessed and used -- the implementation remains the same.

Pre-C++23, to perfectly forward the object itself (e.g., in a generic accessor or wrapper function), you needed all four overloads:

struct MyClass {
    void foo() & { /* lvalue non-const implementation */ }
    void foo() const & { /* lvalue const implementation */ }
    void foo() && { /* rvalue non-const implementation */ }
    void foo() const && { /* rvalue const implementation */ }
};

We want to be able to write a single member function template that can handle all combinations of const, non-const, l-value, and r-value references for the object on which it is called.

Enter C++23 deducing this.

With C++23 this keyword, we can condense all the perfect forwarding overloads into one generic implementation:

#include <utility>

struct MyClass {
    template <typename Self>
    void foo(this Self&& self) {
        // Use std::forward to preserve the original value category and constness
        internal_implementation(std::forward<Self>(self));
    }
};

As another benefit, we also get a cleaner implementation of the Curiously Recurring Template Pattern (CRTP). In traditional CRTP, the base class has to "know" the derived type via a template parameter. To access derived data, we have to cast the this pointer, which can be confusing for complex inheritance hierarchies, or if you accidentally cast to the wrong type.

template <typename Derived>
struct Base {
    void interface() {
        // Confusion: 'this' is Base*, not Derived*. 
        // You MUST cast it, or the compiler won't see Derived's methods.
        static_cast<Derived*>(this)->implementation();
    }
};

struct Derived : Base<Derived> {
    void implementation() { /* ... */ }
};

If you have struct Wrong : Base<Derived>, the static_cast in the base class will compile but cause undefined behavior at runtime because it casts Base to a sibling type instead of itself. Confusing and hard to debug!

With C++23's deduced this, we can shift the template from class to function level, take out the static_cast, and have better type safety! The compiler can deduce the exact type of the object the method was called on, ensuring self is always the correct type.

struct Base {
    template <typename Self>
    void interface(this Self&& self) {
        // No static_cast needed! 
        // 'self' is automatically deduced as the Derived type.
        self.implementation();
    }
};

struct Derived : Base {
    void implementation() { /* ... */ }
};

Thirdly, deducing this greatly simplifies recursive lambda syntax. Traditionally, a lambda's body couldn't refer to itself because the object wasn't fully defined until the expression was complete. Here were some awkward workarounds:

  • using std::function:

    #include <functional>
    #include <iostream>
    
    int main() {
        // Declare the std::function object first
        std::function<int(int)> fib = [&](int n) -> int {
            if (n <= 1) {
                return n;
            }
            // 'fib' is in scope here because it was declared previously
            return fib(n - 1) + fib(n - 2);
        };
    
        std::cout << "Fib(10) = " << fib(10) << std::endl; // Output: 55
    }
  • using a generic lambda (with auto&& or const auto& parameter) that accepts itself as an argument to its own call operator, avoiding the overhead of std::function.

    #include <iostream>
    
    int main() {
        // The outer lambda defines the interface for the caller
        auto factorial = [](int n) {
            // The inner, generic lambda performs the actual recursion
            auto fact_impl = [](int n_val, const auto& self) -> int {
                if (n_val <= 1) {
                    return 1;
                }
                // Pass 'self' explicitly in the recursive call
                return n_val * self(n_val - 1, self);
            };
            // Initial call passes the implementation to itself
            return fact_impl(n, fact_impl);
        };
    
        std::cout << "Factorial(5) = " << factorial(5) << std::endl; // Output: 120
    }
  • or the competitive programming variant defining the self-passing behavior directly:

    #include <iostream>
    
    int main() {
        // A single generic lambda that takes itself as the first argument
        auto fact = [&](auto&& self, int x) -> int {
            return x <= 1 ? 1 : x * self(self, x - 1);
        };
    
        std::cout << "Factorial(5) = " << fact(fact, 5) << std::endl; // Output: 120
    }
  • in a functional manner, a generic Y-combinator (no, not the startup incubator, though that is what YC is named after) higher-order function can be defined. The Y-combinator takes a function that accepts "itself" as its first argument and handles the recursive binding automatically. (no, I don't really know what's happening here. #TODO learn lambda calculus and explain!)

    // A simple generic Y-combinator implementation (C++14/17)
    template<class R, class... Args>
    auto Y = [](auto f) {
        auto action = [=](auto action) -> std::function<R(Args...)> {
            return [=](Args&&... args) -> R {
                // The 'f' is called with a recursive version of itself (action(action))
                return f(action(action), std::forward<Args>(args)...);
            };
        };
        return action(action);
    };
    
    // Usage with a factorial function "generator"
    auto fact = Y<int, int>([](auto&& self, int val) {
        if (val < 2) {
            return 1;
        }
        return val * self(val - 1);
    });
    
    std::cout << fact(5); // Output: 120

But now we can use the self parameter as an alias for the lambda object within its scope, allowing for direct recursive calls.

// The 'self' parameter makes the lambda object available inside its body
auto factorial = [](this const auto& self, int n) -> int {
    if (n <= 1) return 1;
    return n * self(n - 1); // Directly call 'self'
};
// Call it normally, no extra arguments needed
static_assert(factorial(5) == 120, "factorial() incorrect"); //

Also, deducing this is once again a feature that works strongly with C++26 reflection! Deduction directly provides the instance of the object, including its derived type. Reflection will then allow you to introspect that deduced type at compile-time to see its members, methods, and attributes.

How implemented

#TODO

Sources (some)

https://www.sandordargo.com/blog/2022/02/16/deducing-this-cpp23

https://devblogs.microsoft.com/cppblog/cpp23-deducing-this/

https://antonkw.github.io/scala/y-combinator/