Концепция умного указателя 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_ptr
2 это обертка над простым указателем T*
. Наверное, все программисты на C++ использовали этот класс.
Одна из самых популярных причин использования этого указателя — динамический полиморфизм.
Если мы на этапе компиляции не «знаем», объект какого именно класса будем создавать в некой точке выполнения, то из-за этого не знаем значение, на которое надо увеличивать указатель стека, а значит такой объект на стеке создавать нельзя — можем создать его только в куче.
Пусть у нас есть виртуальный класс IEngine
и его наследники TSteamEngine
, TRocketEngine
, TEtherEngine
. Объект «какого-то наследника IEngine
, известного в run-time» это чаще всего именно std::unique_ptr
, в таком случае память для объекта аллоцируется в куче.
template Однако что делать, если класс Есть шанс, что Чем новее стандарт 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.comT
не имеет 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*
…template
T
иметь свой экземпляр ops
: template
static_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_t
11: 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_traits
std::is_base
: #define STATIC_PTR_INHERITED_BUFFER_SIZE(Tp, size) \
namespace sp { \
template
Реализация: sp: static_ptr
ops
и буфер для объекта: template
reset
, который удаляет объект — этот метод часто используется: // destruct the underlying object
void reset() noexcept(std::is_nothrow_destructible_v
std::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
template
std::unique_ptr
: // accessors
Base* get() noexcept {
return ops_ ? reinterpret_cast
std::make_unique
и std::make_shared
, сделаем метод sp::make_static
: template
Как пользоваться sp: static_ptr
static_ptr
13.static_ptr
и то, что происходит с объектами внутри них.Бенчмарк
google/benchmark
14. Код для этого есть в репозитории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_SingleSmartPointer
sp::static_ptr
, который представляет собой stack-only аналог std::unique_ptr
.Ссылки