frost

C++23 Deducing This for Advanced Inheritance Patterns

by Frost · January 28, 2026

Every C++ developer who has written a fluent builder or used the Curiously Recurring Template Pattern has felt the friction: static_cast<Derived&>(*this) scattered through base classes, four near-identical overloads for const/non-const/lvalue/rvalue combinations, and template boilerplate that obscures the actual logic. C++23's explicit object parameter — known informally as "deducing this" — addresses all of these problems with a single language feature defined in P0847R7.

The Explicit Object Parameter

A non-static member function can now declare its first parameter with the this keyword, making the implicit object parameter explicit:

struct Widget {
    template <typename Self>
    auto&& name(this Self&& self) {
        return std::forward<Self>(self).name_;
    }

private:
    std::string name_;
};

When you call w.name() on an lvalue Widget w, Self deduces to Widget&. Call it on a const Widget& and Self becomes const Widget&. Call it on an rvalue and Self is Widget&&. One function replaces four overloads.

De-Quadruplication

The motivating example from the proposal itself is std::optional::value. Before C++23, a correct implementation required four overloads:

template <typename T>
class optional {
    T& value() & { return storage_; }
    const T& value() const& { return storage_; }
    T&& value() && { return std::move(storage_); }
    const T&& value() const&& { return std::move(storage_); }
};

All four bodies are identical modulo qualifiers. With deducing this:

template <typename T>
class optional {
    template <typename Self>
    auto&& value(this Self&& self) {
        return std::forward<Self>(self).storage_;
    }
};

The compiler generates the appropriate overload at each call site. There is no runtime overhead — this is a compile-time mechanism that produces the same machine code as hand-written overloads.

Replacing CRTP

CRTP lets a base class call into its derived class without virtual dispatch, but the ergonomics are rough:

template <typename Derived>
struct Addable {
    Derived operator+(const Derived& rhs) const {
        Derived result = static_cast<const Derived&>(*this);
        result += rhs;
        return result;
    }
};

struct Vec2 : Addable<Vec2> {
    float x, y;
    Vec2& operator+=(const Vec2& rhs) {
        x += rhs.x;
        y += rhs.y;
        return *this;
    }
};

The static_cast is error-prone and the template parameter feels redundant. With deducing this, the base class deduces the derived type directly:

struct Addable {
    template <typename Self>
    auto operator+(this Self self, const Self& rhs) {
        self += rhs;
        return self;
    }
};

struct Vec2 : Addable {
    float x, y;
    Vec2& operator+=(const Vec2& rhs) {
        x += rhs.x;
        y += rhs.y;
        return *this;
    }
};

No template parameter on the base class. No static_cast. The compiler deduces Self as Vec2 at the call site.

Mixin Composition

This pattern scales naturally to mixin-style composition. Consider a set of orthogonal capabilities:

struct Printable {
    template <typename Self>
    void print(this const Self& self) {
        std::cout << self.to_string() << '\n';
    }
};

struct Serializable {
    template <typename Self>
    std::string serialize(this const Self& self) {
        return "{\"value\": \"" + self.to_string() + "\"}";
    }
};

struct Name : Printable, Serializable {
    std::string value;
    std::string to_string() const { return value; }
};

Each mixin accesses the derived class through self without any template parameter threading. Adding a new mixin is just another base class.

Recursive Lambdas

Before C++23, writing a recursive lambda required either std::function (with heap allocation and type erasure overhead) or a Y-combinator wrapper. Deducing this makes it direct:

auto fib = [](this auto self, int n) -> int {
    if (n <= 1) return n;
    return self(n - 1) + self(n - 2);
};

int result = fib(10);

The lambda receives itself as the first argument through the explicit object parameter. No heap allocation, no indirection.

Fluent Builder Pattern

Builders that return *this for method chaining have a classic inheritance problem: the base builder returns the base type, breaking the chain in derived builders. Deducing this solves it:

struct QueryBuilder {
    template <typename Self>
    Self&& select(this Self&& self, std::string_view cols) {
        self.query_ += "SELECT " + std::string(cols);
        return std::forward<Self>(self);
    }

    template <typename Self>
    Self&& from(this Self&& self, std::string_view table) {
        self.query_ += " FROM " + std::string(table);
        return std::forward<Self>(self);
    }

protected:
    std::string query_;
};

struct PostgresBuilder : QueryBuilder {
    template <typename Self>
    Self&& returning(this Self&& self, std::string_view cols) {
        self.query_ += " RETURNING " + std::string(cols);
        return std::forward<Self>(self);
    }
};

Calling PostgresBuilder{}.select("*").from("users").returning("id") works because each method returns the actual derived type, not the base.

Constraints and Limitations

Explicit object parameters have clear boundaries:

  • Cannot be virtual — the feature is a compile-time mechanism, incompatible with runtime dispatch
  • Cannot be static — the function is inherently tied to an object
  • Cannot carry const, volatile, or ref-qualifiers — the qualifiers are deduced from the object expression instead
  • The explicit object parameter must be the first parameter

Compiler Support

Compiler Version Flag
MSVC 17.2+ /std:c++latest
GCC 14+ -std=c++23
Clang 18+ -std=c++23

The feature is well-supported across all three major compilers. If your project targets C++23, deducing this is ready for production use. For codebases that need to maintain C++20 compatibility, the traditional CRTP pattern remains the fallback.