Многообразие функциональных обёрток

от автора

A polymorphic function object wrapper

В далёком 2002-ом комитет по стандартизации C++ посетил пропозал, предлагавший ввести шаблонный класс, некий обобщенный «указатель на функцию», способный работать как с простыми указателями на функции, указателями на методы классов, так и с произвольными функциональными объектами [1].

В качестве мотивации к принятию он приводил несколько весомых юзкейсов: колбэки и функции высших порядков.

Колбэки

Без function

class mouse_move_listener { public:   virtual void on_mouse_move(int x, int y) = 0; };  class mouse_loc_printer : public mouse_move_listener { public:   virtual void on_mouse_move(int x, int y) {     std::cout << '(' << x << ", " << y << "\n";   } };  mouse_move_listener* move_listener;  void fire_mouse_move(int x, int y) {   if (move_listener)     move_listener->on_mouse_move(x, y); }

С function

std::function<void (int x, int y)> on_mouse_move;  void fire_mouse_move(int x, int y) {   if (on_mouse_move)     on_mouse_move(x, y); }  on_mouse_move = [](int x, int y) {   std::cout << '(' << x << ", " << y << "\n"; }

Функции высших порядков

function<int (int x, int y)> arithmetic_operation(char k) {   switch (k) {     case '+': return plus<int>();     case '-': return minus<int>();     case '*': return multiplies<int>();     case '/': return divides<int>();     case '%': return modulus<int>();     default: assert(0);   } }

Комитет долго думал и решил: фиче быть! Так в стандартную библиотеку C++11 вошел всем нам хорошо знакомый класс function.

И всё было бы хорошо. Но этот класс вышел в свет неадаптированным к наступившим реалиям.

Что если мы захотим захватить в лямбду что-нибудь некопируемое и лишь перемещаемое? Например, unique_ptr? (godbolt)

#include <functional> #include <memory>  struct widget { };  int main() {   auto w_ptr = std::make_unique<widget>();   std::function<void()> f = [w_ptr = std::move(w_ptr)] { /*...*/ }; }

Этот код просто не скомпилируется:

/opt/.../function.h:439:69: error: std::function target must be copy-constructible   439 | static_assert(is_copy_constructible<__decay_t<_Functor>>::value,       |                                                           ^~~~~

Так появилась необходимость в новой функциональной обертке, на этот раз способной работать с некопируемыми типами. И комитет по стандартизации, теперь в 2015-ом, посетил новый пропозал [2]

A polymorphic wrapper for all Callable objects

Он предлагал ввести unique_function — вариант function, но поддерживающий некопируемые типы. Дело шло без спешки и в итоге этот пропозал в его конечной редакции [3] включили только в C++23 под названием move_only_function. И, мало того, что он позволял работать с некопируемыми типами, в нем были исправлены обнаруженные за прошедшее десятилетие проблемы function:

  1. Беды с const-корректностью.

  2. Отсутствие поддержки cv/ref/noexcept-квалифицированных типов.

Рассмотрим каждую из них поподробнее на примерах.

Беды с const-корректностью

Пример доступен на godbolt

#include <functional>  int main() {     // Допустим, у нас есть function объект, инициализированный     // мутабельной лямбдой:     std::function<void(void)> func{[&]() mutable {     // ...     }};     // Мы можем его вызывать и всё хорошо:     func();      // Но если нам захочется создать константную ссылку на него,     // рассчитывая, что у нас не получится вызвать function,     // если объект, захваченный в него, является изменяемым     const auto &ref{func};     // То мы не достигнем своих целей! Код ниже скомпилируется!     // const - не const — для function это ничего не значит     ref(); }

Отсутствие поддержки cv/ref/noexcept-квалифицированных типов

Пример доступен на godbolt

#include <functional>  void f() noexcept { }  int main() {   // — Что-что? Сохранить noexcept спецификатор?   // Нет, нас дизайнили в 2002 году! Получай ошибку компиляции!     std::function<void() noexcept> fnp = f;      // — Ну давай хотя бы const?   // — Отвали. Compiler Error     std::function<void() const> fcp = []() const {};      // ... }

И все бы хорошо, но…

Copyable function

«Подождите! — воскликнул внимательный читатель, — вот эти все косяки, а почему бы их не исправить в обычном function, не вечность же теперь жить с ними, имея один move_only_function, где они исправлены, и не имея копируемый аналог move_only_function?

Комитет подумал и решил: «В этих словах есть смысл, но мы не готовы исправить сам function. Мы не готовы нарушить устои и традиции, на которые с 2011-го года полагаются тысячи программистов».

Внимательный читатель кивнул, удалясь для дум прочь. Но вскоре вернулся с пропозалом, предлагавшим ввести новый класс, вариант move_only_function, но поддерживающий копируемые типы. Так в C++26 приняли copyable_function [4].

Первый пример

auto lambda{[&]() /*const*/ { … }};  copyable_function<void(void)> func0{lambda}; const auto & ref0{func0};  // Все хорошо, мы вызываем // неконстантную (по переданной сигнатуре) функцию // через неконстантный объект func0(); // Compilation error: пытаемся вызвать // неконстантную (по переданной сигнатуре) функцию // через константный объект ref0(); // operator() is NOT const!  // ---  copyable_function<void(void) const> func1{lambda}; const auto & ref1{func1};  // All is OK. Вызываем константную функцию // 1) Через неконстантный объект func1(); // 2) Через константный объект ref1(); // operator() is const!

Второй пример

auto lambda{[&]() mutable { … }};  copyable_function<void(void)> func{lambda}; const auto & ref{func};  // Все хорошо: вызываем // неконстантную функцию через неконстантный объект func(); // Compilation error: пытаемся вызвать // неконстантную функцию через константный объект ref();  // Compilation error: не можем // сохранить неконстантную функцию как константную copyable_function<void(void) const> tmp{lambda};

Теперь-то точно все должны быть довольны? 🙂

Не скажите! Что мы упустили — и function, и move_only_function, и copyable_function — все они по своей семантике владеют завернутыми в них функциями. А что если, если все, что нам нужно — это невладеющий reference на callable объект? Который, между прочим, может быть даже ни копируемым, ни перемещаемым.

Non-owning reference to a Callable

Встречайте function_ref, принятый в C++26 [5]!

Теперь мы можем вызывать то, что нельзя ни копировать, ни перемещать:

#include <functional>  struct A {     A() { }     A(const A&) = delete;     A(A&&) = delete;     void operator()() { } };  int main() {     A obj;      // Compile errors:     std::function<void(void)> func1 = obj;     std::move_only_function<void(void)> func2 = std::move(obj);     std::copyable_function<void(void)> func3 = obj;      // All is OK:     std::function_ref<void(void)> func4 = obj;     func4(); }

И, вспоминая о колбэках, послуживших одним из ведущих юзкейсов, демонстрирующих полезность function. А разве функция, принимающая колбэк, желает владеть им? Желает оверхед в виде аллокаций? В большинстве случаев — нет.

Так что, на самом деле, это больше юзкейс для function_ref:

data retry(size_t times, function_ref<data()> action) { // ... }

Теперь то нам функциональных оберток хватит? Или, может, самое время написать пропозал, предлагающий какую-нибудь еще? 🙂

Опубликовано при поддержке C++ Moscow


ссылка на оригинал статьи https://habr.com/ru/articles/788524/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *