﻿# Концепция умного указателя static\_ptr<T\> в C\+\+

В C\+\+ есть несколько "умных указателей" – 'std::unique\_ptr', 'std::shared\_ptr', 'std::weak\_ptr'\. 

Также есть более нестандартные умные указатели, например в [boost](https://www.boost.org/doc/libs/1_79_0/libs/smart_ptr/doc/html/smart_ptr.html): _intrusive\_ptr_, _local\_shared\_ptr_\.

![0983_smartpointer_ru/image1.png](https://import.viva64.com/docx/blog/0983_smartpointer_ru/image1.png)


> Мы опубликовали и перевели эту статью с разрешения правообладателя\\\. Автор статьи – Евгений Шульгин, \\\(email – izaronplatz@gmail\\\.com\\\)\\\. Оригинал опубликован на сайте \[Habr\]\(https://habr\.com/ru/post/665632/\)\\\.

В этой статье мы рассмотрим новый вид умного указателя, который можно назвать _static\_ptr_\. Больше всего он похож на _std::unique\_ptr_ без динамической аллокации памяти\.

![0983_smartpointer_ru/image2.png](https://import.viva64.com/docx/blog/0983_smartpointer_ru/image2.png)

## 'std::unique\_ptr<T\>'

[_std::unique\_ptr<T\>_](https://en.cppreference.com/w/cpp/memory/unique_ptr) — это обёртка над простым указателем _T\*_\. Наверное, все программисты на C\+\+ использовали этот класс\.

Одна из самых популярных причин использования этого указателя – динамический полиморфизм\.

Если мы на этапе компиляции не "знаем", объект какого именно класса будем создавать в некой точке выполнения, то из\-за этого не знаем значение, на которое надо увеличивать указатель стека, а значит такой объект на стеке создавать нельзя – можем создать его только в куче\.

Пусть у нас есть полиморфный класс _IEngine_ и его наследники _TSteamEngine_, _TRocketEngine_, _TEtherEngine_\. Объект "какого\-то наследника _IEngine_, известного в runtime" – это чаще всего именно _std::unique\_ptr<IEngine\>_, в таком случае память для объекта аллоцируется в куче\. 

![0983_smartpointer_ru/image3.png](https://import.viva64.com/docx/blog/0983_smartpointer_ru/image3.png)

Рисунок 1\. std::unique\_ptr<IEngine\> с объектами разного размера

## Аллокация маленьких объектов

Аллокации в куче нужны для "больших объектов" _\(std::vector_ с кучей элементов, etc\.\), в то время как стек лучше подходит для "маленьких объектов"\.

В Linux для получения размера стека для процесса можно запустить:

```cpp
ulimit -s
```

По умолчанию покажется невысокое число, на моих системах это 8192 KiB \= 8 MiB\. В то время как память из кучи можно хавать гигабайтами\.

Аллокация большого количества маленьких объектов фрагментирует память и негативно отражается на кэше процессора\. 

## Объекты на стеке

Как можно сделать объект, аналогичный _std::unique\_ptr_, но полностью стековый?

В C\+\+ есть [_std::aligned\_storage_](https://en.cppreference.com/w/cpp/types/aligned_storage), который даёт сырую память на стеке, и в этой памяти при помощи конструкции [_placement new_](https://www.geeksforgeeks.org/placement-new-operator-cpp/) можно создать объект нужного класса T\. Надо проконтролировать, чтобы памяти было не меньше, чем _sizeof\(T\)\._

Таким образом, за счёт микроскопического оверхеда \(несколько незанятых байтов\) на стеке можно создавать объекты произвольного класса\.

## 'sp::static\_ptr<T\>'

Имея намерение сделать _stack\-only_ аналог _std::unique\_ptr<T\>,_ я решил поискать уже готовые реализации, потому что идея, казалось бы, лежит на поверхности\.

Придумав такие слова как _stack\_ptr_, _static\_ptr_ и пр\. и поискав их на GitHub, я нашёл вменяемую реализацию в проекте [ceph](https://github.com/ceph/ceph), в [_ceph/static\_ptr\.h_](https://github.com/ceph/ceph/blob/master/src/common/static_ptr.h) и увидел там некоторые полезные идеи\. Впрочем, в проекте этот класс используется мало где, и в реализации есть ряд существенных промахов\.

Реализация может выглядеть так – есть сам буфер для объекта \(в виде _std::aligned\_storage_\); и какие\-то данные, которые позволяют правильно рулить объектом: например, вызывать деструктор именно того типа, который сейчас содержится в _static\_ptr_\.

![0983_smartpointer_ru/image4.png](https://import.viva64.com/docx/blog/0983_smartpointer_ru/image4.png)

Рисунок 2\. sp::static\_ptr<IEngine\> с объектами разного размера \(буфер на 32 байта\)

## Реализация: насколько сложен 'move'?

Здесь я опишу пошаговую реализацию и множество подводных камней, которые могут всплыть\.

Сам класс _static\_ptr_ я решил поместить внутри _namespace sp_ \(от _static pointer_\)\.

Реализации контейнеров, умных указателей, и прочих вещей — это вообще одни из самых сложных программ на C\+\+, потому что приходится задумываться над вещами, про которые в нормальных проектах не подозревают\.

Допустим, мы хотим вызвать _move_\-конструктор из одного участка памяти в другой\. Можно написать так:

```cpp
template <typename T>
struct move_constructer
{
  static void call(T *lhs, T *rhs)
  {
    new (lhs) T(std::move(*rhs));
  }
};

// call `move_constructer<T>::call(dst, src);
```

Однако, что делать, если класс _T_ не имеет _move_\-конструктора?

Есть шанс, что_ T_ имеет _move_\-оператор присваивания, тогда надо использовать его\. Если и его нет, то надо "сломать" компиляцию\.

Чем новее стандарт C\+\+, тем легче писать код для таких вещей\. Получим такой код \(скомпилируется в C\+\+17\):

```cpp
template <typename T>
struct move_constructer
{
  static void call(T *lhs, T *rhs)
  {
    if constexpr (std::is_move_constructible_v<T>)
    {
      new (lhs) T(std::move(*rhs));
    }
    else if constexpr (   std::is_default_constructible_v<T>
                       && std::is_move_assignable_v<T>)
    {
      new (lhs) T();
      *lhs = std::move(*rhs);
    }
    else
    {
      []<bool flag = false>()
      { 
        static_assert(flag, "move constructor disabled");
      }();
    }
  }
};
```

\(на 10 строке слом компиляции в виде _static\_assert_ происходит с [хаком](https://stackoverflow.com/questions/38304847/constexpr-if-and-static-assert/64354296#64354296)\)

Однако неплохо бы еще указывать _noexcept_\-спецификатор, когда это возможно\. В C\+\+20 получаем такой код, настолько простой, насколько возможно в данный момент:

```cpp
template <typename T>
struct move_constructer
{
  static void call(T *lhs, T *rhs)
    noexcept (std::is_nothrow_move_constructible_v<T>)
    requires (std::is_move_constructible_v<T>)
  {
    new (lhs) T(std::move(*rhs));
  }

  static void call(T *lhs, T *rhs)
    noexcept (   std::is_nothrow_default_constructible_v<T>
              && std::is_nothrow_move_assignable_v<T>)
    requires (  !std::is_move_constructible_v<T>
              && std::is_default_constructible_v<T>
              && std::is_move_assignable_v<T>)
  {
    new (lhs) T();
    *lhs = std::move(*rhs);
  }
```

Аналогичным образом с разбором кейсов можно сделать структуру _move\_assigner_\. Можно было бы ещё сделать _copy\_constructer_ и _copy\_assigner_, но в нашей реализации они не нужны\. В _static\_ptr_ будут удалены _copy constructor_ и _copy assignment operator_ \(как и в _unique\_ptr_\)\.

## Реализация: 'std::type\_info' на коленке

Хотя в _static\_ptr_ может лежать любой объект, нам всё равно нужно как\-то "знать" о том, что за тип там лежит\. Например, чтобы мы могли вызывать деструктор именно этого объекта и делать прочие вещи\.

После нескольких попыток я выработал такой вариант – нужна структура _ops_:

```cpp
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\*_\.\.\.

```cpp
template <typename T, typename Functor>
void call_typed_func(void *dst, void *src)
{
  Functor::call(static_cast<T*>(dst), static_cast<T*>(src));
}

template <typename T>
void destruct_func(void *dst)
{
  static_cast<T*>(dst)->~T();
}
```

И теперь мы можем для каждого типа _T_ иметь свой экземпляр _ops_:

```cpp
template <typename T>
static constexpr ops ops_for
{
  .move_construct_func = &call_typed_func<T, move_constructer<T>>,
  .move_assign_func = &call_typed_func<T, move_assigner<T>>,
  .destruct_func = &destruct_func<T>,
};

using ops_ptr = const ops *;
```

_static\_ptr_ будет хранить внутри себя ссылку на _ops\_for<T\>, _где _T_ – это класс объекта, который сейчас лежит в _static\_ptr_\.

## Реализация: I like to move it, move it

Копировать _static\_ptr_ будет нельзя – можно только мувать в другой _static\_ptr_\. Выбор способа мува зависит от того, что за тип у объектов, которые лежат в этих двух _static\_ptr_:

1. Оба _static\_ptr_ пустые \(_dst\_ops \= src\_ops \= nullptr_\): ничего не делать\.
1. _static\_ptr_ содержат один и тот же тип \(_dst\_ops \= src\_ops_\): делаем _**move assign**_ и разрушаем объект в _src_\.
1. _static\_ptr_ содержат разные типы \(_dst\_ops \!\= src\_ops_\): разрушаем объект в _dst_, делаем _**move construct**_, разрушаем объект в _src_, делаем присваивание _dst\_ops \= src\_ops_\.

Получится такой метод:

```cpp
// 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;
  }
}
```

## Реализация: размер буфера и выравнивание

Сейчас надо решить, какой будет дефолтный размер буфера и какое будет [выравнивание](https://en.cppreference.com/w/c/language/object) потому, что _std::aligned\_storage_ требует знать эти два значения\.

Понятно, что выравнивание класса\-наследника может превышать выравнивание [класса\-предка](https://godbolt.org/z/cbjoo3v59)\. Поэтому выравнивание должно быть максимально возможным, которое только бывает\. В этом нам поможет тип [_std::max\_align\_t_](https://en.cppreference.com/w/cpp/types/max_align_t):

```cpp
static constexpr std::size_t align = alignof(std::max_align_t);
```

На моих системах это значение 16, но где\-то могут быть нестандартные значения\.

Кстати, память из кучи \(из _malloc_\) тоже выравнивается по максимально возможному _alignment_, автоматически\.

Дефолтный размер буфера можно поставить в 16 байт или в _sizeof\(T\)_ – что будет больше\.

```cpp
template <typename T>
struct static_ptr_traits
{
  static constexpr std::size_t buffer_size =
    std::max(static_cast<std::size_t>(16), sizeof(T));
};
```

Понятно, что почти всегда это значение нужно будет переопределять на свою величину, чтобы помещались объекты всех классов\-наследников\. Желательно сделать это в виде макроса, чтобы было быстро писать\. Можно сделать такой макрос для переопределения размера буфера в одном классе:

```cpp
#define STATIC_PTR_BUFFER_SIZE(Tp, size)               \
namespace sp                                           \
{                                                      \
  template<> struct static_ptr_traits<Tp>              \
  {                                                    \
    static constexpr std::size_t buffer_size = size;   \
  };                                                   \
}

// example:
STATIC_PTR_BUFFER_SIZE(IEngine, 1024)
```

Однако этого недостаточно, чтобы выбранный размер "наследовался" всеми классами\-наследниками нужного\. Для этого можно сделать ещё один макрос с использованием _std::is\_base_:

```cpp
#define STATIC_PTR_INHERITED_BUFFER_SIZE(Tp, size)          \
namespace sp                                                \
{                                                           \
    template <typename T> requires std::is_base_of_v<Tp, T> \
    struct static_ptr_traits<T>                             \
    {                                                       \
        static constexpr std::size_t buffer_size = size;    \
    };                                                      \
}

// example:
STATIC_PTR_INHERITED_BUFFER_SIZE(IEngine, 1024)
```

## Реализация: 'sp::static\_ptr<T\>'

Теперь можно привести реализацию самого класса\. У него всего два поля – ссылка на _ops_ и буфер для объекта:

```cpp
template <typename Base> requires(!std::is_void_v<Base>)
class static_ptr
{
private:
    static constexpr std::size_t buffer_size =
      static_ptr_traits<Base>::buffer_size;
    
    static constexpr std::size_t align = alignof(std::max_align_t);

    // Struct for calling object's operators
    // equals to `nullptr` when `buf_` contains no object
    // equals to `ops_for<T>` when `buf_` contains a `T` object
    ops_ptr ops_;

    // Storage for underlying `T` object
    // this is mutable so that `operator*` and `get()` can
    // be marked const
    mutable std::aligned_storage_t<buffer_size, align> buf_;

    // ...
```

В первую очередь реализуем метод _reset_, который удаляет объект – этот метод часто используется:

```cpp
    // destruct the underlying object
    void reset() noexcept(std::is_nothrow_destructible_v<Base>)
    {
      if (ops_)
      {
        (ops_->destruct_func)(&buf_);
        ops_ = nullptr;
      }
    }
```

Реализуем базовые конструкторы по аналогии с _std::unique\_ptr_:

```cpp
    // 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<Base>)
    {
      reset();
      return *this;
    }
```

Теперь можно реализовать _move constructor_ и _move assignment operator_\. Чтобы принимался тот же тип, надо сделать так:

```cpp
    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_ для других типов\. Другой тип должен влезать в буфер и быть наследником текущего типа:

```cpp
  template <typename Derived>
  struct derived_class_check
  {
    static constexpr bool ok = sizeof(Derived) <= buffer_size
                            && std::is_base_of_v<Base, Derived>;
  };
```

И надо объявить "друзьями" все инстанцирования класса:

```cpp
  // support static_ptr's conversions of different types
  template <typename T> friend class static_ptr;
```

Тогда два предыдущих метода можно переписать так:

```cpp
  template <typename Derived = Base>
  static_ptr(static_ptr<Derived> &&rhs)
    requires(derived_class_check<Derived>::ok)
      : ops_ { nullptr }
  {
    move_construct(&buf_, ops_, &rhs.buf_, rhs.ops_);
  }

  template <typename Derived = Base>
  static_ptr& operator=(static_ptr<Derived> &&rhs)
    requires(derived_class_check<Derived>::ok)
  {
    move_construct(&buf_, ops_, &rhs.buf_, rhs.ops_);
    return *this;
  }
```

Копирование запрещено:

```cpp
  static_ptr(const static_ptr &) = delete;

  static_ptr& operator=(const static_ptr &) = delete;
```

Деструктор разрушает объект в буфере:

```cpp
  ~static_ptr()
  {
    reset();
  }
```

Для создания объекта в буфере сделаем метод _emplace_\. Старый объект удалится \(если он есть\), в буфере создастся новый, и обновится указатель на _ops_:

```cpp
  // in-place (re)initialization
  template <typename Derived = Base, typename ...Args>
  Derived& emplace(Args &&...args)
    noexcept(std::is_nothrow_constructible_v<Derived, Args...>)
    requires(derived_class_check<Derived>::ok)
  {
    reset();
    Derived* derived = new (&buf_) Derived(std::forward<Args>(args)...);
    ops_ = &ops_for<Derived>;
    return *derived;
  }
```

Методы\-аксесоры сделаем такие же, как у _std::unique\_ptr_:

```cpp
  // accessors
  Base* get() noexcept
  {
    return ops_ ? reinterpret_cast<Base*>(&buf_) : nullptr;
  }

  const Base* get() const noexcept
  {
    return ops_ ? reinterpret_cast<const Base*>(&buf_) : nullptr;
  }

  Base& operator*() noexcept { return *get(); }
  const Base& operator*() const noexcept { return *get(); }

  Base* operator&() noexcept { return get(); }
  const Base* operator&() const noexcept { return get(); }

  Base* operator->() noexcept { return get(); }
  const Base* operator->() const noexcept { return get(); }

  operator bool() const noexcept { return ops_; }
};
```

По аналогии с _std::make\_unique_ и _std::make\_shared_, сделаем метод _sp::make\_static_:

```cpp
template <typename T, class ...Args>
static static_ptr<T> make_static(Args &&...args)
{
  static_ptr<T> ptr;
  ptr.emplace(std::forward<Args>(args)...);
  return ptr;
}
```

Реализация доступна на [GitHub](https://github.com/Izaron/static_ptr)\!

## Как пользоваться sp::static\_ptr<T\>?

Это просто\! Я сделал юнит\-тесты, которые показывают лайфтайм объектов, живущих внутри [_static\_ptr_](https://github.com/Izaron/static_ptr/blob/main/test/test_derived.cc)\.

В тесте можно посмотреть типичные сценарии работы со _static\_ptr_ и то, что происходит с объектами внутри них\.

## Бенчмарк

Для бенчмарков я использовал библиотеку [_google/benchmark_](https://github.com/google/benchmark)\. Код для этого есть в [репозитории](https://github.com/Izaron/static_ptr/blob/main/benchmark/benchmark.cc)\.

Я рассмотрел два сценария, в каждом из них проверяется _std::unique\_ptr_ и _sp::static\_ptr_:

* Создание умного указателя и вызов метода объекта\.
* Итерирование по вектору из 128 умных указателей, у каждого вызывается метод\.

В первом сценарии выигрыш у _sp::static\_ptr_ должен быть за счёт отсутствия аллокации, во втором сценарии за счёт локальности памяти\. Хотя, конечно, понятно, что компиляторы очень умные и умеют хорошо оптимизировать "плохие" сценарии в зависимости от флагов оптимизации\.

Запустим бенчмарк в сборке _Debug_:

```cpp
***WARNING*** Library was built as DEBUG. Timings may be affected.
--------------------------------------------------------------------------------
Benchmark                           Time               CPU            Iterations
--------------------------------------------------------------------------------
SingleUniquePointer               207 ns            207 ns               3244590
SingleStaticPointer              39.1 ns           39.1 ns              17474886
IteratingOverUniquePointers      3368 ns           3367 ns                204196
IteratingOverStaticPointers      1716 ns           1716 ns                397344
--------------------------------------------------------------------------------
```

В сборке _Release_:

```cpp
--------------------------------------------------------------------------------
Benchmark                           Time               CPU            Iterations
--------------------------------------------------------------------------------
SingleUniquePointer              14.5 ns           14.5 ns              47421573
SingleStaticPointer              3.57 ns           3.57 ns             197401957
IteratingOverUniquePointers       198 ns            198 ns               3573888
IteratingOverStaticPointers       195 ns            195 ns               3627462
--------------------------------------------------------------------------------
```

Таким образом, есть определенный выигрыш в перфомансе у _sp::static\_ptr_, который представляет собой _stack\-only_ аналог _std::unique\_ptr_\.

```cpp
```