Skip to content
C++
Language
since C++23
Intermediate

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++23

An 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

cpp
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:

cpp
// 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&

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.

cpp
// 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>

Recursive lambdas

The lambda receives itself as its first argument, eliminating any need for std::function or fixed-point combinators:

cpp
// C++23 — self is the closure object; taken by value for stateless lambda
auto fib = [](this auto self, int n) -> int {
    if (n <= 1) return n;
    return self(n - 1) + self(n - 2);
};
std::println("{}", fib(10));   // 55   (std::println requires C++23 <print>)

// Memoised variant — closure owns state; take self by reference
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:

cpp
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 self

Constrained mixin methods

Unconstrained this auto&& on a base class will instantiate for any type at all call sites. For mixin patterns, constrain the deduced type:

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

Best Practices

  • Use this auto&& + std::forward<decltype(self)> for forwarding returns. It replaces all four ref-qualified overloads and propagates value category correctly.
  • 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 than int get() const. Reserve explicit object parameters for cases where they remove genuine duplication or enable type deduction.
  • Prefer deducing-this CRTP 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).

Common Pitfalls

this keyword is unavailable in the body.

cpp
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
    }
};

Virtual functions are forbidden. The language prohibits combining virtual with an explicit object parameter. There is no workaround.

cpp
struct Base {
    virtual void process(this Base& self);  // ERROR
};

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.

cpp
void bad(int x, this auto& self);  // ERROR: explicit object parameter must be first

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


See Also

  • crtp — classic CRTP pattern that explicit object parameters partially replace
  • lambda — recursive lambdas are the most common everyday use
  • templates — abbreviated function templates underpin the this auto&& syntax