Документ «deducing this», принятый в последний стандарт C++, вводит новый, третий тип методов классов, сочетающий в себе свойства двух уже существующих: нестатических и статических, открывающий перед нами новые горизонты:
-
Дедупликация большого количества кода
-
Вытеснение CRTP (Curiously Recuring Template Pattern) на свалку истории, его замена более простой и очевидно понятной записью
-
Рекурсивные лямбды
И другое.
Но прежде чем рассмотреть само нововведение и его практические применения, углубимся немного в историю и попытаемся понять, почему в нем собственно возникла необходимость.
Мотивация
Начиная с C++03, методы могут иметь cv-квалификаторы, так что стали возможны сценарии, когда есть необходимость как в const, так и не-const перегрузке определенного метода (для краткости воздержимся от рассмотрения volatile перегрузок).
Во многих случаях между их логикой нет никакой разницы — отличаются лишь квалификаторы используемых типов, так что приходится или копировать определение, подгоняя квалификаторы, или использовать такие механизмы как const_cast:
class TextBlock { public: char const& operator[](size_t position) const { // ... return text[position]; } char& operator[](size_t position) { return const_cast<char&>( static_cast<TextBlock const&>(*this)[position] ); } // ... };
Начиная с C++11, методы могут иметь также и ref-квалификаторы, так что теперь вместо двух перегрузок одного метода нам могут понадобиться четыре: &, const&, &&, const&&, и у нас есть три способа решить данную задачу (ссылка на код, приведенный ниже):
-
Писать реализацию одного метода четырежды
-
Делегировать три перегрузки четвертой, используя
static_castиconst_cast -
Использовать вспомогательную шаблонную функцию

Но ни один из этих способов не избавляет нас от необходимости четырежды определять практически один и тот же метод.
Если бы могли написать что-то вроде функции ниже, но ведущей себя как член класса, это решило бы все наши проблемы:
template <typename T> class optional { // ... template <typename Opt> friend decltype(auto) value(Opt&& o) { if (o.has_value()) { return forward<Opt>(o).m_value; } throw bad_optional_access(); } // ... };
Но мы не можем. Точней, не могли. До C++23
Мечты воплощаются в реальность
Теперь же мы можем определять методы, явно принимающие в качества аргумента объект, над которым они были вызваны:
struct X { template <typename Self> void foo(this Self&&) { } }; void example(X& x) { x.foo(); // Self = X& move(x).foo(); // Self = X X{}.foo(); // Self = X }
Таким образом, теперь мы можем переписать всю ту простыню кода, реализующую метод value у optional, гораздо более компактно, меньше чем в десяток строк:
template <typename T> struct optional { template <typename Self> constexpr auto&& value(this Self&& self) { if (!self.has_value()) { throw bad_optional_access(); } return forward<Self>(self).m_value; }
Мы подчинили своим целям механизм вывода типов во всей своей мощи! И важно отметить, что это все тот же механизм, проявляющий себя в обычных шаблонных функциях и методах.
Deducing this не вносит никаких изменений в правила вывода типов, он лишь позволяет явное объявление объектного параметра (explicit object parameter) в списке аргументов методов. Параметра, который до этого в методах присутствовал лишь неявно (implicit object parameter), в виде указателя this.
Важно отметить, что вывод типов способен выводить производные типы:
struct X { template <typename Self> void foo(this Self&&, int); }; struct D : X { }; void example(X& x, D& d) { x.foo(1); // Self=X& move(x).foo(2); // Self=X d.foo(3); // Self=D& }
На методы, объявленные с явным объектным параметром, также накладывается ряд ограничений:
-
Они не могут быть объявлены статическими
Причина этого заключается в том, что хоть и внешне эти функции выглядят и ведут себя как обычные нестатические методы, но внутренне они ведут себя в точности как статические: в них недоступен указатель this, и единственный способ взаимодействовать в них с объектом класса — через явный объектный параметр.
Кроме того, указатель на такие методы — это указатель на функцию, а не член класса.
-
Они не могут быть объявлены виртуальными
-
Они не могут быть объявлены с использованием ref или cv квалификаторов
Так как объявление явного объектного параметра уже несет в себе всю необходимую информацию о его типе:

Но есть нюансы
Однако с большой силой приходит и большая ответственность. Так, используя новый тип методов, мы должны постоянно помнить о том, насколько могуществен механизм вывода типов:
struct B { int i = 0; template <typename Self> auto&& f1(this Self&& self) { return forward<Self>(self).i; } }; struct D: B { double i = 3.14; };
Тогда как B().f1() в коде выше вернет ссылку на B::i, D().f5() вернет ссылку на D::i, так как self является ссылкой на D.
Если же мы хотим получать ссылку на B::i всегда, нам необходимо явно предусмотреть это в коде:
template <typename Self> auto&& f1(this Self&& self) { return forward<Self>(self).B::i; }
Другой опасностью на нашем пути может служить проблема приватного наследования. Рассмотрим следующий код:
class B { int i; public: template <typename Self> auto&& get(this Self&& self) { return forward<Self>(self).B::i; } }; class D: private B { double i; public: using B::get; }; D().get(); // error
Мы, казалось бы, предохранились как могли. Однако недостаточно. Мы не можем получить доступ к B::i из D, так как наследование является приватным.
Но и для этой проблемы существует решение:
class B { int i; public: template <typename Self> auto&& get(this Self&& self) { // like_t — функция, применяющая ref и cv квалификаторы // первого переданного типа ко второму // Например, like_t<int& double> = double& return ((like_t<Self, B>&&)self).i; } }; class D : private B { double i; public: using B::get; }; D().get(); // now ok, and returns B::i
Выполнив приведение в стиле си для избежания проверки доступа, мы реализовали желаемое поведение.
Практическое применение
Итак, немножко поговорив о сущности нововведения и об опасностях, которые могут нас поджидать при его использовании, перейдем к рассмотрению некоторых из его практических приложений.
Дедупликация кода
Это первое и самое очевидное. Помимо уже рассмотренных примеров, приведу еще несколько:

Внедрение методов в классы-наследники
Да-да, вы подумали правильно. CRTP (Curiously Recurring Template Pattern) больше не нужен. И хоть код от использования deducing this в простейшем случае ниже не становится короче, но очевидно становится более простым и интуитивно понятным.

Рекурсивные лямбды
О чем я ранее еще не упоминал — это то, что теперь мы можем определять лямбды с явным объектным параметром, благодаря чему становится возможным определение рекурсивных лямбд:
auto fib = [](this auto self, int n) { if (n < 2) return n; return self(n-1) + self(n-2); };
struct Leaf { }; struct Node; using Tree = variant<Leaf, Node*>; struct Node { Tree left; Tree right; }; int num_leaves(Tree const& tree) { return visit(overload( // <-----------------------------------+ [](Leaf const&) { return 1; }, // | [](this auto const& self, Node* n) -> int { // | return visit(self, n->left) + visit(self, n->right); // <----+ } ), tree); }
Передача self по значению
Передача self по значению открывает для нас возможность более естественного выражения желаемой семантики, например когда смысл метода заключается в одном лишь возврате модифицированной копии:
struct my_vector : vector<int> { auto sorted(this my_vector self) -> my_vector { sort(self.begin(), self.end()); return self; } };
Но, что многие справедливо посчитают более важным применением, позволяет в некоторых случаях достичь лучшей производительности.
Например, все мы знаем, что маленькие типы во избежание порождения лишних уровней косвенности (indirection) лучше передавать по значению.
К одному из таких типов относится std::string_view. Который мы можем передавать по значению всюду: в наши функции, конструкторы, другие методы. Но только не в его собственные методы. До принятия deducing this у разработчиков стандартной библиотеки не было способов избежать разыменований указателя this в собственных методах std::string_view.
Теперь же мы можем переписать все его методы, не выполняющие модификаций, с передачей явного объектного параметра по значению, практически бесплатно этим получая улучшение производительности:
template <class charT, class traits = char_traits<charT>> class basic_string_view { private: const_pointer data_; size_type size_; public: constexpr const_iterator begin(this basic_string_view self) { return self.data_; } constexpr const_iterator end(this basic_string_view self) { return self.data_ + self.size_; } constexpr size_t size(this basic_string_view self) { return self.size_; } constexpr const_reference operator[](this basic_string_view self, size_type pos) { return self.data_[pos]; } };
Заключение
На самом деле, это далеко не все, что можно сказать про deducing this, но самое главное и основное. Целью данной статьи не являлось углубление в детали.
Если вы хотите постичь все нюансы — вы можете обратиться к оригинальному документу.
Также довольно интересы и увлекательны статьи, написанные некоторыми из его авторов: «C++23’s Deducing this: what it is, why it is, how to use it» (Sy Brand), «Copy-on-write with Deducing this» (Barry Revzin).
Любите плюсы и будьте счастливы.
ссылка на оригинал статьи https://habr.com/ru/post/722668/
Добавить комментарий