Класс function
представляет собой полиморфную оболочку для функциональных объектов, а именно для тех, чей тип удовлетворяет Callable и CopyConstructible.
Это может быть указатель на функцию, лямбда, класс с перегруженным operator()
и т.д.
Хранимый функциональный объект в терминах стандарта принято называть target
.
Храним полем unique_ptr<concept>
, где concept
— класс с виртуальными функциями (например, operator()
).
При конструировании из функционального объекта типа F
создаём в динамической памяти model<F>
, где хранится сам F
, а также определены те самые виртуальные функции.
Функциональные объекты часто не имеют состояния вообще, или имеют, но небольшое.
Поэтому возникает естественное желание легковесные объекты хранить непосредственно внутри function
, вместо указателя на динамическую память.
К сожалению, нам больше не подойдёт unique_ptr
, ведь в случае small object не будет указателя на динамическую память.
Во-первых, не очень понятно, какой у такого unique_ptr
'а должен быть Deleter
.
Во-вторых, а как тогда правильно мувать small object'ы?
Разделим хранилище и логику на два поля:
- выровненный массив байт, где будет лежать либо сам функциональный объект (в случае small object), либо указатель на него;
- указатель на дескриптор — набор вспомогательных функций для оперирования хранилищем (может быть выражен классом с виртуальными функциями).
В зависимости от типа функционального объекта, будем хранить указатели на дескрипторы с разными динамическими типами.
Не имеет смысл когда-либо иметь два инстанса дескриптора с одинаковым динамическим типом — они эквивалентны. Мы можем этим воспользоваться, чтобы избежать каких-либо динамических аллокаций для дескрипторов — будем просто хранить по одному инстансу каждого дескриптора статически, и ссылаться на них.
Дефолтный конструктор создаёт пустой (empty) function
, который не хранит в себе никакой функциональный объект, а при вызове operator()
кидает bad_function_call
.
Для него удобно сделать отдельный дескриптор.
В отличие от стандартной библиотеки, ваш function
должен гарантировать небросающие мувающие операции (конструктор и оператор присваивания).
К сожалению, в общем случае это невозможно при SOO, поэтому используйте эту оптимизацию только для тех T
, у которых мув и сам не бросает.
Метод F* target<F>()
должен возвращать указатель на текущий функциональный объект, если его динамический тип совпадает с F
, и nullptr
иначе.
Работа с буфером для SOO должна быть корректна с точки зрения алиасинга и выравнивания.