В методе требуется единая реализация для const-, non-const- и rvalue-вызовов без набора перегрузок. Как механизм C++23 позволяет выразить это через явный параметр объекта?
В C++23 для этого используется явный параметр объекта — explicit object parameter, также известный как deducing this. Метод объявляет объект через параметр вида this Self&& self, после чего Self выводится с учётом cv-квалификаторов и категории значения объекта.
Так одна шаблонная функция может обслуживать lvalue-, const-lvalue-, rvalue- и const-rvalue-объекты. Внутри такого метода нет неявного this: к объекту обращаются через имя явного параметра.
До C++23 различия между const-объектами, неконстантными объектами, lvalue и rvalue обычно требовали нескольких перегрузок метода с разными cv- и ref-квалификаторами. Эти перегрузки часто содержали одинаковую логику и отличались только типом объекта, через который она выполнялась.
Явный параметр объекта добавили, чтобы выразить эту зависимость непосредственно в параметрах функции. Подход также упрощает шаблонный код, уменьшает дублирование и делает тип объекта доступным для ограничений, decltype и условного выбора реализации.
Рассмотрим класс, у которого метод возвращает ссылку на поле. Для неконстантного объекта должна возвращаться изменяемая ссылка, для константного — ссылка на const, а для временного объекта может потребоваться отдельная политика доступа или перемещения.
Если написать независимые перегрузки вручную, реализации могут разойтись. Если оставить только неконстантную перегрузку, вызов для const-объекта не скомпилируется; если возвращать некорректный тип для временного объекта, можно получить ошибочную семантику времени жизни или лишнее копирование.
Синтаксис this Self&& self означает, что объект вызова становится первым явным параметром функции. Self выводится как конкретный тип объекта, включая const, volatile и ссылочность, а Self&& работает по правилам forwarding-ссылки.
Для неконстантного lvalue Self выводится как Buffer&, для const lvalue — как const Buffer&, а для rvalue — как Buffer. Применение std::forward<Self> сохраняет категорию значения исходного объекта, поэтому обращение к полю имеет соответствующую cv-квалификацию и категорию.
Такой метод вызывается обычным синтаксисом через объект, но внутри нельзя использовать неявный this. Нельзя одновременно объявлять явный параметр объекта и применять cv- или ref-квалификаторы метода: свойства объекта задаются самим типом параметра.
Механизм не означает, что все перегрузки всегда следует заменять одной шаблонной функцией. Если для разных категорий объектов нужны существенно разные контракты, ограничения или возвращаемые типы, отдельные перегрузки могут быть понятнее. Кроме того, следует внимательно проверять время жизни результата: возврат ссылки на поле временного объекта остаётся опасным после завершения времени жизни этого объекта.
Явный параметр объекта может быть ограничен концептом или использоваться для рекурсивного вызова через self. Он также позволяет писать методы, которые работают с производными типами без отдельного CRTP-слоя, если такая полиморфная по статическому типу объекта логика действительно нужна.
В библиотечном типе view нужно предоставить один метод доступа к элементу для четырёх категорий объектов. Вариант с четырьмя перегрузками наиболее явно фиксирует интерфейс, но дублирует реализацию и повышает риск расхождения между версиями.
Вариант с обычным шаблонным методом без явного параметра объекта не позволяет напрямую вывести cv-квалификаторы и категорию самого объекта вызова. CRTP может решить задачу, но добавляет дополнительный уровень наследования и усложняет публичную модель типа.
Выбранный вариант — явный параметр объекта с forwarding-ссылкой и ограничениями при необходимости. Он оставляет одну реализацию, сохраняет свойства объекта и позволяет компилятору проверить вызовы для всех поддерживаемых категорий. При этом для опасных rvalue-вызовов API дополнительно ограничивают доступ или возвращаемый тип, чтобы не выдавать ссылку, которая будет использоваться после уничтожения временного объекта.
Почему в теле нельзя писать this->value?
У метода с явным параметром объекта нет неявного указателя this. Объект представлен обычным параметром, например self, поэтому доступ выполняется через self.value или std::forward<Self>(self).value. Это важно не только синтаксически: имя параметра можно передавать, ограничивать концептами и использовать как обычный объект.
Что произойдёт с типом Self для lvalue и rvalue?
Для неконстантного lvalue обычно получается Self = T&, для const lvalue — Self = const T&, а для rvalue — Self = T или соответствующий const-квалифицированный тип. Поэтому Self&& после схлопывания ссылок сохраняет категорию исходного объекта, а std::forward<Self>(self) восстанавливает её внутри метода.
Зачем всё равно может понадобиться несколько перегрузок?
Единая шаблонная реализация удобна только при общей семантике. Если lvalue должен возвращать ссылку, а rvalue — перемещённое значение, либо если для const и неконстантного объекта действуют разные ограничения, отдельные перегрузки могут точнее выразить контракт. Явный параметр объекта не устраняет проектную необходимость различать операции, он лишь предоставляет более компактный способ выразить случаи с одинаковой логикой.