# std::function С++11 предоставил разработчикам очень удобный класс-шаблон для описания абстрактных вызываемых объектов: ```C++ std::function f = /* все что угочно, что можно вызвать как f(args) и результатом будет R */ ``` Благодаря технике _type-erasure_ (стирание типа), `std::function` может хранить в себе что угодно. У этого, конечно, есть цена — посредственная производительность: конкретный вызываемый объект должен быть перемещен в кучу, выделение памяти, динамическая диспетчеризация вызова... Если мы не пишем чего-то высоконагруженного, то цена не очень высока. Однако, благодаря тому же самому стиранию типов и тому, как оно реализовано, `std::function` обладает еще некоторыми потрясающими спецэффектами! ### Спецэффект 1. Вариантность С этим понятием знакомы далеко не все разработчики, так что начнем с примера. Пожалуй, я не позволю себе использовать забитый пример с классами `Animal` и `Dog`. Вместо этого у меня будет `InputDevice` и `Keyboard`. Вопрос: если `Keyboard` это подтип `InputDevice`, то является ли `Containter` подтипом `Container`? С точки зрения абстрактной достаточно высокоуровневой иерархии в системе типов — вполне себе. Коробка клавиатур это коробка устройств ввода. И так, например, будет в языках Java, Kotlin или Rust ```Rust // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=679a17722f69cf5178fb751f42c8a543 trait InputDevice { fn input(&self) -> char; } trait Keyboard: InputDevice { fn lock_keys(&self); } struct MBox(T); // Коробка клавиатур это коробка устройств ввода! // Keyboard is subtype of InputDevice // MBox is subtype of MBox fn relabel(b: MBox) -> MBox { b } ``` Теоретики скажут, что коробка ковариантна типу содержимого! Типы контейнеров и типы содержимого вложены согласованно. Поэтому **ко**вариантна. А бывает и обратная ситуация. Например, работник, который умеет чинить клавиатуры, совсем не факт что может чинить произвольные устройства ввода. А вот если наоборот — то без проблем. Он и клавиатуру вам починит. ```Rust // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ef93662f6bf210b61483a06ac56b4b4a trait Senior { fn repair(&self, _: impl InputDevice); } trait Junior { fn repair(&self, _: impl Keyboard); } impl Junior for M { fn repair(&self, k: impl Keyboard) { Senior::repair(self, k) } } // Keyboard is subtype of InputDevice // Senior (repairs InputDevice) is subtype of Junior (repairs only keyboards) fn demote(m: impl Senior) -> impl Junior { m } ``` Вложенность типов работников противоположна вложенности типов объектов, с которыми они работают. Они **контра**вариантны. Шаблоны в C++ **ин**вариантны: отношения между типами-контейнерами никак не зависят от типов элементов... Так по крайней мере должно быть и так и задумывалось. Если бы нe внезапное исключение в виде `std::function`! ```C++ // https://godbolt.org/z/oecYYcfvj class InputDevice { public: virtual ~InputDevice() = default; virtual char input() = 0; }; class Keyboard : public InputDevice { public: char input() override { return '*'; } }; // мы можем сделать так Keyboard* keyboard = ...; // ведь клавиатура это устройство ввода InputDevice* device = keyboard; std::vector box_of_keyboards { keyboard, keyboard, keyboard }; // Compilation error. Шаблоны инвариантны. Такого оператора присваивания нет std::vector box_of_input_devs = box_of_keyboard; // Но! std::function keyboard_maker = ...; // Отлично компилируется! std::function ковариантен по возвращаемому значению std::function input_device_maker = keyboard_maker; std::function senior = ...; // Тоже компилируется! std::function контравариантен по принимаемым параметрам std::function junion = senior; ``` С одной стороны выглядит очень даже здорово и правильно. А с другой стороны оно, разумеется, работает не потому что `std::function` на самом деле поддерживает вариантность. Так происходит из-за неявного приведения типов аргументов и возвращаемого значения, а также повторного стирания типов (со всеми накладными расходами). ```C++ std::function senior = [](auto){}; // Аллокация и перемещение лямбды на кучу! Стерли тип лямбды внутри std::function junion = std::move(senior); // Типы разные. Шаблоны инвариантны. Еще одна аллокация! и перемещение исходной std::function на кучу. Стираем ее тип. ``` А если цепочки передачи таких функций с изменением типов будут более длинными — становится уже не так здорово. И проблему можно усугубить тем, что такая "вариантность" работает не только лишь с указателями/ссылками на наследуемые классы в сигнатуре функции. Как было замечено ранее: оно работает из-за неявного приведения типов. А значит мы можем сделать так ```C++ // https://godbolt.org/z/cjfTz5dEW std::function by_view = [](std::string_view v){ std::cout << v; }; std::function by_str_ref = by_view; std::function by_str_val = by_str_ref; std::function by_char_ptr = by_str_val; by_char_ptr("hello"); ``` И если туда попадет `nullptr`... Мы помним что будет: ``` Program returned: 139 Program stderr terminate called after throwing an instance of 'std::logic_error' what(): basic_string: construction from null is not valid Program terminated with signal: SIGSEGV ``` И разумеется при наличии таких цепочек сохранять переданные в аргументах ссылки становится особенно сомнительным занятием. Проблему с переизбытком аллокаций C++26 предлагает решать с помощью `std::function_ref` — невладеющих ссылок на вызаваемый объект. Создайте ваш объект один раз и храните, а дальше передавайте на него ссылку с удобным интерфейсом. Вариантность остается в комплекте с возможностью получить dangling reference на другой `std::function_ref` в цепочке присваиваний. ### Спецэффект 2. Отломанный const Усердно стирая типы, `std::function` перестарался и подтер пробрасывание `const`. ```C++ int main() { const auto counter = [count = 0]() mutable { return count++; }; // cannot call: mutable lambda requires mutable access to operator() // counter(); const std::function f = counter; f(); // this is fine! const is not propagated! } ``` Ну подтер и подтер... Ничего ж страшного. Код компилируется и вызывается — это ж главное! А некорректный код, который тоже компилируется из-за этого, просто не нужно писать... О важности правильного пробрасывания const, а особенно с приходом C++23 и deducing this, можно вспомнить один забавный факт о популярном фреймворке Qt: в нем свои свобственные контейнеры с Copy-on-Write поведением по умолчанию. И это приводит к тому, что совершенно очевидная read-only итерация по контейнеру внезапно [вызывает копирование всего контейнера!](https://doc.qt.io/qt-6/containers.html#implicit-sharing-iterator-problem) А также к инвалидациям ссылок и другим занятным последствиям и порчей памяти. ```C++ // Бах! range based for вызывает begin()/end() методы, // которые в не const версии вызывают копирование! for (const auto& x: qlist) {}; // Вот так надежнее и корректнее for (const auto& x: std::as_const(qlist)) {}; ``` Если мы теперь вернемся обратно к `std::function` и будем передавать в нее вызываемый объект с существенно разными `const` и non-`const` перегрузками `operator()` мы можем получить довольно неприятные результаты: ```C++ // https://godbolt.org/z/KcWxoYqPo struct Proxy { int& operator()(int index) { return data[index]; } int operator()(int index) const { return data.at(index); } std::map& data; }; int main() { std::map data = { {42, 42}}; const std::function f = Proxy{data}; f(43); // expect throw? for (auto [k, v]: data){ std::cout << k << " " << v << "\n"; } } ``` Но `const` был потерян, так что результатом будет: ``` 42 42 43 0 ``` В C++26 проблему решили: используйте, пожалуйста `std::copyable_function`. ```C++ // const -- часть сигнатуры! // также можно дописывать noexcept -- это еще одно улучшение std::copyable_function f = Proxy{data}; ``` Чинить старый `std::function` не стали по соображениям обратной совместимости со старым кодом, который 1. может полагаться на ее странное поведение с проглатыванием const. 2. рисково может использовать `std::function` в C++ ABI — и изменение в сигнатуре `operator()` могут его сломать (хотя и так гарантий по нему не дается) Читатель может заинтересоваться, а почему для исправленной версии выбрано такое странное название. А это из-за еще одной проблемы `std::function` ### Спецэффект 3. move-only не поддерживается ```C++ std::function f = [data = std::make_unique(42)](int x) { return *data + x; }; ``` ``` /opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/bits/std_function.h:439:69: error: static assertion failed: std::function target must be copy-constructible 439 | static_assert(is_copy_constructible<__decay_t<_Functor>>::value, | ^~~~~ ``` Для решения этой проблемы C++26 ввел `std::move_only_function`. А `std::copyable_function` добавили уже на замену старого `std::function`, который поддерживает только копируемые типы. Возможно, в C++29 `std::function` будет помечен как устаревший и deprecated. ## Полезные ссылки 1. [Copyable Function proposal](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2548r6.pdf) 2. [Variance](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)) 3. [Type Erasure](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Type_Erasure)