Before C++23, CRTP (class Derived : public Base<Derived>) was the main tool for static polymorphism without a vtable. The cost: a templated base, noisy syntax, long compiler diagnostics, and the risk of passing the wrong Derived into Base<...>.

With deducing this, the base can be an ordinary (non-template) class: the type of the object of the call is deduced in this Self&& / this Self&.

Fluent interface (method chaining)

Problem: base methods return *this, but the chain needs the derived type.

CRTP

template <typename Derived>
class BuilderCRTP {
public:
    Derived& set_id(int id) {
        id_ = id;
        return static_cast<Derived&>(*this);
    }

protected:
    int id_{0};
};

class UserBuilder : public BuilderCRTP<UserBuilder> {
public:
    UserBuilder& set_name(std::string name) {
        name_ = std::move(name);
        return *this;
    }

private:
    std::string name_;
};

UserBuilder builder;
builder.set_id(42).set_name("Oleksii");

Downsides: the base is a template; easy to write class Bad : public BuilderCRTP<OtherClass>; needs static_cast.

C++23

class BuilderModern {
public:
    template <typename Self>
    Self& set_id(this Self& self, int id) {
        self.id_ = id;
        return self; // already Derived& — no static_cast
    }

protected:
    int id_{0};
};

class UserBuilder : public BuilderModern {
public:
    UserBuilder& set_name(std::string name) {
        name_ = std::move(name);
        return *this;
    }

    void print() const {
        std::cout << "ID: " << id_ << ", Name: " << name_ << '\n';
    }

private:
    std::string name_;
};

UserBuilder builder;
builder.set_id(42).set_name("Oleksii");

Self is deduced as UserBuilder& when called on a UserBuilder.

Static interface without a vtable

CRTP:

template <typename Derived>
struct Printable {
    void print() const {
        static_cast<const Derived*>(this)->print_impl();
    }
};

struct Order : public Printable<Order> {
    void print_impl() const { std::cout << "Order #123\n"; }
};

C++23:

struct Printable {
    template <typename Self>
    void print(this const Self& self) {
        self.print_impl();
    }
};

struct Order : public Printable {
    void print_impl() const { std::cout << "Order #123\n"; }
};

Comparison

CRTP Deducing this
Base Base<Derived> ordinary Base
Inheritance class D : public B<D> class D : public B
Access to Derived static_cast self parameter
Wrong type in the base possible always the type of the call object

This does not erase CRTP from every design, but for the common “base needs the real call type” cases, deducing this removes the recursive template.