ядро языкаcore language

deducing this

Явное указание типа неявного параметра this как первого шаблонного параметра метода — снимает необходимость в CRTP для статического полиморфизма и убирает дублирование const/non-const/lvalue/rvalue перегрузок.Explicitly spelling out the implicit this parameter's type as a method's first template parameter — removes the need for CRTP in static polymorphism and eliminates duplicated const/non-const/lvalue/rvalue overloads.

часто используетсяcommonly used частый вопрос: чем заменяет CRTPcommon question: what it replaces CRTP with

Что этоWhat it is

"Deducing this" — возможность объявить метод класса с явным первым параметром this Self&& self, где Self — шаблонный параметр, автоматически выводимый компилятором как тип объекта, через который метод был вызван. Это заменяет неявный, скрытый параметр this, который раньше нельзя было ни поименовать, ни параметризовать по типу."Deducing this" is the ability to declare a class method with an explicit first parameter this Self&& self, where Self is a template parameter automatically deduced by the compiler as the type of the object the method was called through. This replaces the implicit, hidden this parameter, which previously could neither be named nor parameterized by type.

до C++23 — CRTP и дублирование перегрузокbefore C++23 — CRTP and duplicated overloads
template<class Derived>
struct Base {
    void print() {
        static_cast<Derived*>(this)->do_print();  // ручной static_cast, шаблонный Base// manual static_cast, a templated Base
    }
};
struct Derived : Base<Derived> {  // многословное самоссылающееся наследование// verbose, self-referential inheritance
    void do_print() { /* ... *//* ... */ }
};
C++23 — deducing this, без CRTPC++23 — deducing this, no CRTP
struct Base {
    template<class Self>
    void print(this Self&& self) {
        self.do_print();  // Self выводится как тип наследника, без static_cast// Self is deduced as the derived type, no static_cast
    }
};
struct Derived : Base {  // обычное, не шаблонное наследование// ordinary, non-templated inheritance
    void do_print() { /* ... *//* ... */ }
};

Зачем ввелиWhy it was introduced

Два независимых, но родственных источника boilerplate-кода в C++ существовали десятилетиями без прямого решения на уровне языка:Two independent but related sources of boilerplate had existed in C++ for decades with no direct language-level fix:

01

CRTP требовал многословного шаблонного объявления и ручного static_castCRTP required verbose template declarations and a manual static_cast

Curiously Recurring Template Pattern — class Derived : public Base<Derived> — был единственным способом получить "тип наследника" внутри метода базового класса без virtual. Он требовал шаблонного базового класса, самоссылающегося объявления наследника и явного static_cast<Derived*>(this) в каждом методе, где нужен был доступ к производному типу — три источника ошибок ради одной концептуальной идеи.The Curiously Recurring Template Pattern — class Derived : public Base<Derived> — was the only way to get the "derived type" inside a base class method without virtual. It required a templated base class, a self-referential derived-class declaration, and an explicit static_cast<Derived*>(this) in every method that needed access to the derived type — three sources of error for a single conceptual idea.

02

const/non-const/lvalue/rvalue перегрузки дублировали тело методаconst/non-const/lvalue/rvalue overloads duplicated the method body

Метод, который должен по-разному вести себя для const- и non-const объекта, или для lvalue и rvalue, требовал до четырёх отдельных перегрузок (const&, &, const&&, &&), почти идентичных по телу и отличающихся только квалификатором самого this — классический источник copy-paste ошибок при изменении логики в одной перегрузке и забытом обновлении остальных трёх.A method needing different behavior for a const vs. non-const object, or for an lvalue vs. an rvalue, required up to four separate overloads (const&, &, const&&, &&), nearly identical in body and differing only in this's qualifier — a classic source of copy-paste bugs when logic changes in one overload and the other three are forgotten.

03

Один шаблонный параметр решает обе задачи сразуA single template parameter solves both problems at once

Сделав this обычным (хоть и специально помеченным) параметром шаблонной функции, компилятор получает возможность выводить его тип точно так же, как для любого другого forwarding-параметра — включая производный класс при вызове через наследника и cv/value-category квалификаторы при вызове через разные виды ссылок — без ручного дублирования кода под каждый случай.By making this an ordinary (albeit specially marked) template function parameter, the compiler can deduce its type exactly like any other forwarding parameter — including the derived class when called through a subclass, and the cv/value-category qualifiers when called through different kinds of references — without manually duplicating code for every case.

Как это работает под капотомHow it works under the hood

Параметр this Self&& self ведёт себя как universal reference (forwarding reference): Self выводится компилятором в момент вызова метода так же, как выводился бы тип шаблонного параметра T&& в обычной функции — если вызов идёт через lvalue, Self становится ссылочным типом (Derived&), если через rvalue — обычным типом (Derived), с сохранением const, если объект был const. Метод при этом остаётся невиртуальным — весь полиморфизм разрешается на этапе компиляции.The this Self&& self parameter behaves like a universal (forwarding) reference: the compiler deduces Self at the call site just as it would deduce a T&& template parameter in an ordinary function — calling through an lvalue makes Self a reference type (Derived&), calling through an rvalue makes it a plain type (Derived), preserving const if the object was const. The method itself stays non-virtual — all polymorphism is resolved at compile time.

struct Base {
    template<class Self>
    void print(this Self&& self) {
        self.do_print();  // вызов через self — не через *this, self и есть выведенный объект// called via self — not *this; self is the deduced object itself
    }
};
struct Widget : Base {
    void do_print() const { std::println("widget"); }
};

Widget w;
w.print();               // Self выводится как Widget& (lvalue)// Self is deduced as Widget& (lvalue)
const Widget cw;
cw.print();              // Self выводится как const Widget&// Self is deduced as const Widget&
Widget{}.print();          // Self выводится как Widget (rvalue)// Self is deduced as Widget (rvalue)

Тот же механизм убирает дублирование перегрузок для метода, который должен вести себя по-разному для lvalue/rvalue самого объекта (классический пример — геттер, возвращающий копию для rvalue-объекта и ссылку для lvalue-объекта, чтобы избежать лишнего копирования там, где это безопасно):The same mechanism removes overload duplication for a method that must behave differently for an lvalue vs. an rvalue object (the classic example — a getter that returns a copy for an rvalue object and a reference for an lvalue object, to avoid an unnecessary copy where it's safe):

struct Widget {
    std::string data_;
    template<class Self>
    auto&& data(this Self&& self) {
        return std::forward<Self>(self).data_;  // одна перегрузка вместо четырёх// one overload instead of four
    }
};

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Статический полиморфизм без CRTP-boilerplateStatic polymorphism without CRTP boilerplate

Библиотека визитора/обходчика, где базовый класс должен вызывать переопределённое поведение наследника, не платя за virtual и не заставляя пользователей библиотеки писать class MyVisitor : public Visitor<MyVisitor>.A visitor/traverser library where the base class must invoke the derived class's overridden behavior, without paying for virtual and without forcing library users to write class MyVisitor : public Visitor<MyVisitor>.

struct Visitor {
    template<class Self>
    void visit_all(this Self&& self, const std::vector<Node>& nodes) {
        for (const auto& n : nodes) self.visit(n);  // self.visit — переопределённый в наследнике метод// self.visit — the method overridden in the subclass
    }
};
struct JsonPrinter : Visitor {
    void visit(const Node& n) { std::println("{{}}", n.name); }
};
02

Одна перегрузка геттера вместо четырёхA single getter overload instead of four

Контейнер-обёртка, где доступ к внутреннему полю должен возвращать копию для временного объекта (чтобы не оставлять висячую ссылку) и ссылку для именованной переменной — без deducing this это требовало четырёх текстуально идентичных перегрузок.A wrapper container where access to an inner field must return a copy for a temporary (to avoid a dangling reference) and a reference for a named variable — without deducing this, that required four textually identical overloads.

struct Wrapper {
    std::vector<int> items_;
    template<class Self>
    auto&& items(this Self&& self) {
        return std::forward<Self>(self).items_;
    }
};
Wrapper w{{1, 2, 3}};
auto& ref = w.items();               // lvalue -> ссылка, без копии// lvalue -> a reference, no copy
auto copy = Wrapper{{4,5}}.items();  // rvalue -> перемещённое значение// rvalue -> a moved-from value
03

Рекурсивные лямбды через deducing thisRecursive lambdas via deducing this

Лямбды в C++23 тоже поддерживают this Self в качестве первого параметра — это позволяет лямбде рекурсивно вызывать саму себя, не объявляя отдельную именованную функцию и не прибегая к std::function-обёртке ради самоссылки.C++23 lambdas also support this Self as their first parameter — this lets a lambda recursively call itself without declaring a separate named function or resorting to a std::function wrapper for self-reference.

auto factorial = [](this auto&& self, int n) -> int {
    return n <= 1 ? 1 : n * self(n - 1);  // self — сама лямбда, вызывается рекурсивно// self — the lambda itself, called recursively
};
factorial(5);  // 120, без std::function и без отдельного объявления// 120, no std::function and no separate declaration
04

Builder-паттерн с одной цепочкой методов на все квалификаторыA builder pattern with a single method chain for all qualifiers

Fluent-интерфейс (builder), где каждый метод должен одинаково хорошо работать и при цепочке вызовов на временном объекте (Builder{}.with_x(1).with_y(2)), и на именованной переменной — с move-семантикой в первом случае и без лишних копий во втором.A fluent (builder) interface where every method must work equally well in a chain on a temporary (Builder{}.with_x(1).with_y(2)) and on a named variable — with move semantics in the first case and no extra copies in the second.

struct Builder {
    std::vector<int> parts_;
    template<class Self>
    auto&& with(this Self&& self, int part) {
        self.parts_.push_back(part);
        return std::forward<Self>(self);  // возвращает self с сохранением его value category// returns self, preserving its value category
    }
};

ГраблиGotchas

self — forwarding reference, забытый std::forward ломает move-семантикуself is a forwarding reference — forgetting std::forward breaks move semantics

Параметр self в this Self&& self — обычная именованная переменная внутри тела метода, а значит сама по себе является lvalue, даже если Self вывелся как невыведенный ссылочный тип (rvalue-контекст). Чтобы передать value category дальше (например, вернуть перемещённое значение поля из rvalue-объекта), нужно явно писать std::forward<Self>(self) — забыв об этом, легко случайно всегда копировать там, где ожидалось перемещение, в точности как с любым forwarding-параметром обычной функции.The self parameter in this Self&& self is an ordinary named variable inside the method body, and is therefore itself an lvalue, even if Self was deduced as a non-reference type (rvalue context). To forward the value category further (e.g. to return a moved-from field value from an rvalue object), you must explicitly write std::forward<Self>(self) — forgetting this easily leads to always copying where a move was expected, exactly as with any forwarding parameter of an ordinary function.

Нельзя совмещать deducing this с virtual на одном методеCan't combine deducing this with virtual on the same method

Метод с явным параметром this Self&& не может быть одновременно объявлен virtual — виртуальные методы по-прежнему требуют классического неявного this, потому что механизм виртуальной диспетчеризации (vtable) несовместим с шаблонной природой deducing this (какой именно код вызывать, определяется в рантайме через таблицу, а не выводится компилятором на этапе компиляции). Deducing this заменяет CRTP для статического полиморфизма, но не является заменой virtual для динамического полиморфизма — это два разных инструмента для двух разных задач.A method with an explicit this Self&& parameter can't simultaneously be declared virtual — virtual methods still require the classic implicit this, because virtual dispatch (the vtable) is incompatible with deducing this's templated nature (which exact code to call is determined at runtime via a table, not deduced by the compiler at compile time). Deducing this replaces CRTP for static polymorphism, but is not a replacement for virtual in dynamic polymorphism — these are two different tools for two different problems.

auto&& this — тоже валидный, более короткий синтаксисauto&& this is also valid, shorter syntax

Не обязательно объявлять отдельный шаблонный параметр Self — можно написать this auto&& self прямо в списке параметров (как в примере с рекурсивной лямбдой выше), и компилятор так же выведет тип. Именованный template<class Self> нужен только тогда, когда на Self нужно ссылаться где-то ещё в сигнатуре или теле (например, для std::forward<Self> или ограничений через requires).You don't have to declare a separate Self template parameter — you can write this auto&& self directly in the parameter list (as in the recursive lambda example above), and the compiler will deduce the type just the same. A named template<class Self> is only needed when Self must be referred to elsewhere in the signature or body (e.g. for std::forward<Self> or constraints via requires).