Многообразие функциональных обёрток
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 on_mouse_move;
void fire_mouse_move(int x, int y, std::function
Функции высших порядков
function arithmetic_operation(char k) {
switch (k) {
case '+': return plus();
case '-': return minus();
case '*': return multiplies();
case '/': return divides();
case '%': return modulus();
default: assert(0);
}
}
Комитет долго думал и решил: фиче быть! Так в стандартную библиотеку C++11 вошел всем нам хорошо знакомый класс function
.
И всё было бы хорошо. Но этот класс вышел в свет неадаптированным к наступившим реалиям.
Что если мы захотим захватить в лямбду что-нибудь некопируемое и лишь перемещаемое? Например, unique_ptr
? (godbolt)
#include
#include
struct widget { };
int main() {
auto w_ptr = std::make_unique();
std::function 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-корректностью
Пример доступен на godbolt
#include
int main() {
// Допустим, у нас есть function объект, инициализированный
// мутабельной лямбдой:
std::function func{[&]() mutable {
// ...
}};
// Мы можем его вызывать и всё хорошо:
func();
// Но если нам захочется создать константную ссылку на него,
// рассчитывая, что у нас не получится вызвать function,
// если объект, захваченный в него, является изменяемым
const auto &ref{func};
// То мы не достигнем своих целей! Код ниже скомпилируется!
// const-не const — для function это ничего не значит
ref();
}
Отсутствие поддержки cv/ref/noexcept-квалифицированных типов
Пример доступен на godbolt
#include
void f() noexcept { }
int main() {
// — Что-что? Сохранить noexcept спецификатор?
// Нет, нас дизайнили в 2002 году! Получай ошибку компиляции!
std::function fnp = f;
// — Ну давай хотя бы const?
// — Отвали. Compiler Error
std::function 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 func0{lambda};
const auto & ref0{func0};
// Все хорошо, мы вызываем
// неконстантную (по переданной сигнатуре) функцию
// через неконстантный объект
func0();
// Compilation error: пытаемся вызвать
// неконстантную (по переданной сигнатуре) функцию
// через константный объект
ref0(); // operator() is NOT const!
// ---
copyable_function func1{lambda};
const auto & ref1{func1};
// All is OK. Вызываем константную функцию
// 1) Через неконстантный объект
func1();
// 2) Через константный объект
ref1(); // operator() is const!
Второй пример
auto lambda{[&]() mutable { … }};
copyable_function func{lambda};
const auto & ref{func};
// Все хорошо: вызываем
// неконстантную функцию через неконстантный объект
func();
// Compilation error: пытаемся вызвать
// неконстантную функцию через константный объект
ref();
// Compilation error: не можем
// сохранить неконстантную функцию как константную
copyable_function tmp{lambda};
Теперь-то точно все должны быть довольны? :)
Не скажите! Что мы упустили — и function
, и move_only_function
, и copyable_function
— все они по своей семантике владеют завернутыми в них функциями. А что если, если все, что нам нужно — это невладеющий reference на callable объект? Который, между прочим, может быть даже ни копируемым, ни перемещаемым.
Non-owning reference to a Callable
Встречайте function_ref
, принятый в C++26 [5]!
Теперь мы можем вызывать то, что нельзя ни копировать, ни перемещать:
#include
struct A {
A() { }
A(const A&) = delete;
A(A&&) = delete;
void operator()() { }
};
int main() {
A obj;
// Compile errors:
std::function func1 = obj;
std::move_only_function func2 = std::move(obj);
std::copyable_function func3 = obj;
// All is OK:
std::function_ref func4 = obj;
func4();
}
И, вспоминая о колбэках, послуживших одним из ведущих юзкейсов, демонстрирующих полезность function
. А разве функция, принимающая колбэк, желает владеть им? Желает оверхед в виде аллокаций? В большинстве случаев — нет.
Так что, на самом деле, это больше юзкейс для function_ref
:
data retry(size_t times, function_ref action) {
// ...
}
Теперь то нам функциональных оберток хватит? Или, может, самое время написать пропозал, предлагающий какую-нибудь еще? :)
Опубликовано при поддержке C++ Moscow