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