Концепция умного указателя static_ptr в C++

В C++ есть несколько «умных указателей» — std::unique_ptr, std::shared_ptr, std::weak_ptr. Также есть более нестандартные умные указатели, например в boost1: intrusive_ptr, local_shared_ptr.
В этой статье мы рассмотрим новый вид умного указателя, который можно назвать static_ptr. Больше всего он похож на std::unique_ptr без динамической аллокации памяти.
std: unique_ptr
std::unique_ptr2 это обертка над простым указателем T*. Наверное, все программисты на C++ использовали этот класс.
Одна из самых популярных причин использования этого указателя — динамический полиморфизм.
Если мы на этапе компиляции не «знаем», объект какого именно класса будем создавать в некой точке выполнения, то из-за этого не знаем значение, на которое надо увеличивать указатель стека, а значит такой объект на стеке создавать нельзя — можем создать его только в куче.
Пусть у нас есть виртуальный класс IEngine и его наследники TSteamEngine, TRocketEngine, TEtherEngine. Объект «какого-то наследника IEngine, известного в run-time» это чаще всего именно std::unique_ptr, в таком случае память для объекта аллоцируется в куче.
Однако что делать, если класс Есть шанс, что Чем новее стандарт C++, тем легче писать код для таких вещей. Получим такой код (скомпилируется в C++17): (на 10 строке слом компиляции в виде Однако неплохо бы еще указывать Аналогичным образом с разбором кейсов можно сделать структуру Хотя в После нескольких попыток я выработал такой вариант — нужна структура И пара вспомогательных функций для перевода И теперь мы можем для каждого типа Копировать Оба Получится такой метод: Сейчас надо решить, какой будет дефолтный размер буфера и какое будет выравнивание9, потому что Понятно, что выравнивание класса-наследника может превышать выравнивание класса-предка10. Поэтому выравнивание должно быть максимально возможным, которое только бывает. В этом нам поможет тип На моих системах это значение 16, но где-то могут быть нестандартные значения. Кстати, память из кучи (из Дефолтный размер буфера можно поставить в 16 байт или в Понятно, что почти всегда это значение нужно будет переопределять на свою величину, чтобы помещались объекты всех классов-наследников. Желательно сделать это в виде макроса, чтобы было быстро писать. Можно сделать такой макрос для переопределения размера буфера в одном классе: Однако этого недостаточно, чтобы выбранный размер «наследовался» всеми классами-наследниками нужного. Для этого можно сделать еще один макрос с использованием Теперь можно привести реализацию самого класса. У него всего два поля — ссылка на В первую очередь реализуем метод Реализуем базовые конструкторы по аналогии с Теперь можно реализовать move constructor и move assignment operator. Чтобы принимался тот же тип, надо сделать так: Однако лучше, если мы сможем принимать И надо объявить «друзьями» все инстанциации класса: Тогда два предыдущих метода можно переписать так: Копирование запрещено: Деструктор разрушает объект в буфере: Для создания объекта в буфере сделаем метод Методы-аксесоры сделаем такие же, как у По аналогии с Реализация доступна на GitHub12! Это просто! Я сделал юнит-тесты, которые показывают лайфтайм объектов, живущих внутри В тесте можно посмотреть типичные сценарии работы со Для бенчмарков я использовал библиотеку Я рассмотрел два сценария, в каждом из них проверяется Создание умного указателя и вызов метода объекта. Итерирование по вектору из 128 умных указателей, у каждого вызывается метод. В первом сценарии выигрыш у Запустим бенчмарк в сборке Debug: В сборке Release: Таким образом, есть определенный выигрыш в перфомансе у Boost.SmartPtr std: unique_ptr — cppreference.com C++ Memory Pool and Small Object Allocator | by Debby Nirwan std: aligned_storage — cppreference.com Placement new operator in C++ — GeeksforGeeks ceph — github.com ceph/static_ptr.h — github.com c++ — constexpr if and static_assert Objects and alignment — cppreference.com godbolt.com — выравнивание класса-наследника больше, чем у класса-предка std: max_align_t — cppreference.com Izaron/static_ptr — github.com Izaron/static_ptr, тест test_derives.cc — github.com google/benchmark — github.com Izaron/static_ptr, бенчмарк benchmark.cc — github.com
template T не имеет move-конструктора? T имеет move-оператор присваивания, тогда надо использовать его. Если и его нет, то надо «сломать» компиляцию.template static_assert происходит с хаком8)noexcept-спецификатор, когда это возможно. В C++20 получаем такой код, настолько простой, насколько возможно в данный момент: template move_assigner. Можно было бы еще сделать copy_constructer и copy_assigner, но в нашей реализации они не нужны. В static_ptr будут удалены copy constructor и copy assignment operator (как и в unique_ptr).Реализация: std: type_info на коленке
static_ptr может лежать любой объект, нам все равно нужно как-то «знать» о том, что за тип там лежит. Например, чтобы мы могли вызывать деструктор именно этого объекта, и делать прочие вещи.ops: struct ops {
using binary_func = void(*)(void* dst, void* src);
using unary_func = void(*)(void* dst);
binary_func move_construct_func;
binary_func move_assign_func;
unary_func destruct_func;
};void* в T*…templateT иметь свой экземпляр ops: templatestatic_ptr будет хранить внутри себя ссылку на ops_for, где T это класс объекта, который сейчас лежит в static_ptr.Реализация: I like to move it, move it
static_ptr будет нельзя — можно только мувать в другой static_ptr. Выбор способа мува зависит от того, что за тип у объектов, которые лежат в этих двух static_ptr: static_ptr пустые (dst_ops = src_ops = nullptr): ничего не делать.static_ptr содержат один и тот же тип (dst_ops = src_ops): делаем move assign и разрушаем объект в src.static_ptr содержат разные типы (dst_ops != src_ops): разрушаем объект в dst, делаем move construct, разрушаем объект в src, делаем присваивание dst_ops = src_ops.// moving objects using ops
static void move_construct(void* dst_buf, ops_ptr& dst_ops,
void* src_buf, ops_ptr& src_ops) {
if (!src_ops && !dst_ops) {
// both object are nullptr_t, do nothing
return;
} else if (src_ops == dst_ops) {
// objects have the same type, make move
(*src_ops->move_assign_func)(dst_buf, src_buf);
(*src_ops->destruct_func)(src_buf);
src_ops = nullptr;
} else {
// objects have different type
// delete the old object
if (dst_ops) {
(*dst_ops->destruct_func)(dst_buf);
dst_ops = nullptr;
}
// construct the new object
if (src_ops) {
(*src_ops->move_construct_func)(dst_buf, src_buf);
(*src_ops->destruct_func)(src_buf);
}
dst_ops = src_ops;
src_ops = nullptr;
}
}Реализация: размер буфера и выравнивание
std::aligned_storage требует знать эти два значения.std::max_align_t11: static constexpr std::size_t align = alignof(std::max_align_t);malloc) тоже выравнивается по максимально возможному alignment, автоматически.sizeof(T) — что будет больше.template#define STATIC_PTR_BUFFER_SIZE(Tp, size) \
namespace sp { \
template<> struct static_ptr_traitsstd::is_base: #define STATIC_PTR_INHERITED_BUFFER_SIZE(Tp, size) \
namespace sp { \
templateРеализация: sp: static_ptr
ops и буфер для объекта: templatereset, который удаляет объект — этот метод часто используется: // destruct the underlying object
void reset() noexcept(std::is_nothrow_destructible_vstd::unique_ptr: // operators, ctors, dtor
static_ptr() noexcept : ops_{nullptr} {}
static_ptr(std::nullptr_t) noexcept : ops_{nullptr} {}
static_ptr& operator=(std::nullptr_t) noexcept(std::is_nothrow_destructible_v static_ptr(static_ptr&& rhs) : ops_{nullptr} {
move_construct(&buf_, ops_, &rhs.buf_, rhs.ops_);
}
static_ptr& operator=(static_ptr&& rhs) {
move_construct(&buf_, ops_, &rhs.buf_, rhs.ops_);
return *this;
}static_ptr для других типов. Другой тип должен влезать в буфер и быть наследником текущего типа: template // support static_ptr's conversions of different types
template template static_ptr(const static_ptr&) = delete;
static_ptr& operator=(const static_ptr&) = delete; ~static_ptr() {
reset();
}emplace. Старый объект удалится (если он есть), в буфере создастся новый, и обновится указатель на ops. // in-place (re)initialization
templatestd::unique_ptr: // accessors
Base* get() noexcept {
return ops_ ? reinterpret_caststd::make_unique и std::make_shared, сделаем метод sp::make_static: templateКак пользоваться sp: static_ptr
static_ptr13.static_ptr и то, что происходит с объектами внутри них.Бенчмарк
google/benchmark14. Код для этого есть в репозитории15.std::unique_ptr и sp::static_ptr: sp::static_ptr должен быть за счет отсутствия аллокации, во втором сценарии за счет локальности памяти. Хотя, конечно, понятно, что компиляторы очень умные и умеют хорошо оптимизировать «плохие» сценарии в зависимости от флагов оптимизации.***WARNING*** Library was built as DEBUG. Timings may be affected.
-------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------
BM_SingleSmartPointer-------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------
BM_SingleSmartPointersp::static_ptr, который представляет собой stack-only аналог std::unique_ptr.Ссылки
