Skip to content
C++

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 unchanged

In 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 contextSelf deduced asReturn 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 parameter

You need a recursive lambda without std::function overhead

Write the first parameter as this auto self and call it recursively

Simple member functions with no const/overload or recursion needs

Stick with implicit this — explicit object parameter adds noise without benefit
← std::expected
std::mdspan coming soon
Sign in to track progress