Язык С++. Умные указатели.

Язык С++. Умные указатели(smart pointers).

Источники информации:
  • свой собственный опыт
  • Скотт Мейерс (Эффективный и современный С++)
  • Бьёрн Страуструп (Программирование: Принципы и практика использования C++)
  • Бьёрн Страуструп (The C++ programming language 4th edition)
  • интернеты
Умный (интеллектуальный) указатель — это указатель(обычный), обернутый в специально написанные для этого классы, которые действуют так же как и встроенные(стандартные) указатели, но благодаря оберткам(классам unique_ptr, shared_ptr, weak_ptr) позволяют избежать основных проблем при работе с указателями, а именно - "висячие указатели", утечки памяти, контроль того, чтобы объект удалился только один раз.
Совет - ВСЕГДА предпочитайте обычным указателям, интеллектуальные.


std::unique_ptr

Ближе всего к стандартным указателям это std::unique_ptr, что на самом деле радует :-) Так как это позволяет использовать их в тех ситуациях когда важны расход памяти и процессорное время. То есть, если вам нужен указатель, просто указатель, но вы хотите избавиться от проблем(не забыть очистить память), то сразу вспоминайте о std::unique_ptr.
Объект std::unique_ptr нужно проинициализировать указателем, полученным с помощью оператора new.
#include <iostream>
#include <memory>
struct sType
{
  sType()  {std::cout << "sType" << std::endl;}
  ~sType() {std::cout << "~sType" << std::endl;}
 
  int mInt{0};
};
 
int main()
{
  {
    std::unique_ptr<sType> uptr{new sType};
  }
  {
    std::unique_ptr<sType> uptr;
    uptr = std::unique_ptr<sType>(new sType);
  }
  return 0;
}

При работе с объектом типа std::unique_ptr мы можем использовать операторы -> и *, точно так же как и при работе с обычным указателем.
  std::unique_ptr<sType> uptr{new sType};
  uptr->mInt   = 10;
  (*uptr).mInt = 20;
Объект типа std::unique_ptr как было сказано выше, почти как и встроенный указатель, но!, нельзя присвоить один unique_ptr другому, то есть по сути(судя из названия) объект является уникальным владельцем указателя, и никому его, добровольно не отдаст.
  std::unique_ptr<sType> uptr_a{new sType};
  std::unique_ptr<sType> uptr_b;
  std::unique_ptr<sType> uptr_b{uptr_a};// compilation error
  uptr_b = uptr_a;                      // compilation error
Для встроенных указателей это обычное дело, когда два указателя хранят один и тот же адрес, что зачастую может приводить к ошибкам.
int main()
{
  int *p1 = new int;
  int *p2 = nullptr;
  *p1 = 100;

  // какой-то код, или много кода

  p2 = p1;

  // какой-то код, или много кода
 
  if(p2 != nullptr)
    delete p2;      // p1 и p2 указывают на одно 
                    // и тоже место
  // какой-то код, или много кода
  int val = *p1;    // переменная 'val' получит не 
                    // то значение которое ожидается
  delete p1; 
  return 0;
}
Если нужно передать значение указателя, и права другому объекту unique_ptr или просто указателю, используйте функцию release() класса unique_ptr.
#include <iostream>
#include <memory>
int main()
{
  // Передаем значение указателя p1 в p2.
  // После отработки функции release() объекта p1, 
  // p1 будет хранить nullptr.
  // Свойство уникальности объекта сохраняется.
  {
    // инициализируем указатель
    std::unique_ptr<int> p1{new int{100}};
    std::unique_ptr<int> p2{p1.release()};
  }
  // или
  {
    // инициализируем указатель
    std::unique_ptr<int> p1{new int{100}};
    int *p2;
    p2 = p1.release(); 

    delete p2; 
  }
  // но ни в коем случае не так!!!
  {
    // инициализируем указатель
    std::unique_ptr<int> p1{new int{100}};
    int *p2;  
    p2 = p1.get();  // p1 и p2 хранят одно 
                    // и то-же значение! 
                    // потенциальная ОШИБКА
    *p2 = 0;
    delete p2; 

  }
  return 0;
}
Главное запомните правило - если взялись работать с умными указателями, то выполняйте работу со значением с помощью интерфейса класса unique_ptr. Если нужно передать значение стандартному указателю, то освободите(если конечно этого требует логика программы) объект unique_ptr от указателя, посредством функции release().
Чтобы освободить объект от владения указателем, без его передачи кому-либо, и вызвать его деструктор собственоручно, не ожидая пока умный указатель это сделает сам, используйте функцию reset().
#include <iostream>
#include <memory>
struct sType
{
  sType()  {std::cout << "sType" << std::endl;}
  ~sType() {std::cout << "~sType" << std::endl;}
};
 
int main()
{
  {
    std::unique_ptr<sType> uptr{new sType};
    // освобождаем память занятую объектом sType
    uptr.reset(nullptr); 
  }
  {
    std::unique_ptr<sType> uptr{new sType};
    // освобождаем память занятую объектом sType
    uptr.reset(); 
  }
  {
    std::unique_ptr<sType> uptr{new sType};

    // Освобождаем память занятую объектом sType
    // Выделяем память для нового объекта.
    // То есть перезаписываем старый объект, новым,
    // предварительно удалив старый.
    uptr.reset(new sType);  
                            
  }
  return 0;
} 
Для доступа к значению указателя, используйте стандартный механизм раз-именования или функцию get()(ниже будет пример).Для обмена двух указателей, используйте функцию swap().
#include <iostream>
#include <memory>
int main()
{
  std::unique_ptr<int> up1{new int(1)};
  std::unique_ptr<int> up2{new int(2)};
 
  std::cout << *up1 << " : " << *(up2.get()) 
            << std::endl;
 
  up1.swap(up2);
  std::cout << *up1 << " : " << *(up2.get()) 
            << std::endl;
  return 0;
} 
Кроме всего прочего, можно настроить свой собственный, так сказать удалятор, свою функцию которая будет освобождать память, это может быть полезно к примеру для логирования.
#include <iostream>
#include <memory>
void del_f(int *aP)
{
  delete aP;
  std::cout << "object of int type was deleted" 
            << std::endl;
}
 
int main()
{
  using del_t = void(*)(int*);
  std::unique_ptr<int, del_t> up1(new int(1), del_f);
  return 0;
}
При описании типа объекта, мы указываем(тип функции которая будет освобождать память, второй аргумент(void(*)(int*))), и вторым аргументом для конструктора, передается имя этой функции(int_deleter).
Также указания функции для освобождения удобно, если выделением памяти занимается подключенная библиотека, в примере ниже, к проекту подключена библиотека написанная на языке С, включена функция которая выделяет память с помощью malloc, соответственно желательно и освобождать с помощью free:
extern "C" char *alloc_malloc(int);
using clng_del = void (*)(void*);
void foo()
{
  unique_ptr<char,clng_del> ap(alloc_malloc(100), free);
}
Аналогичный пример для собственного типа.

#include <iostream>
#include <memory>
struct sType
{
  sType()  
  {
    std::cout << "sType" << std::endl;
  }
  ~sType()
  {
    std::cout << "~sType" << std::endl;
  }
};
void deleter(sType *aP)
{
  std::cout << 
            "deleted is OK" 
            << std::endl;
  delete aP;
}
int main()
{
  using del_t = void(*)(sType*);
  
  std::unique_ptr<sType,del_t> up1(new sType, 
                                   deleter);
  return 0;
} 
Более изящный пример с использованиям функтора(о функторах будет отдельная тема)
#include <iostream>
#include <memory>
struct sType
{
  sType()  {std::cout << "sType" << std::endl;}
  ~sType() {std::cout << "~sType" << std::endl;}
};
 
struct TDeleter
{
    void operator()(sType *aP)
    {
      std::cout << 
                "deleted is OK" 
                << std::endl;
      delete aP;
    }
};
 
int main()
{
  std::unique_ptr<sType, TDeleter> up1(new sType);
  return 0;
}
Или :-) еще один изящный пример с использованием лямбда-выражения(для лямбда выражений будет тоже отдельная тема)
#include <iostream>
#include <memory>
struct sType
{
  sType()  {std::cout << "sType" << std::endl;}
  ~sType() {std::cout << "~sType" << std::endl;}
};
 
int main()
{
  auto deleter = [](sType *aP)
  {
    std::cout << "deleted is Ok" << std::endl;
    delete aP;
  };
 
  std::unique_ptr<sType, 
                  decltype(deleter)> up1(new sType, 
                                         deleter);
  return 0;
}
Для unique_ptr запрещен конструктор копирования и оператор присваивания, поэтому код ниже собираться не будет.
void foo()
{
  auto a = std::make_unique<int>();
  auto b = std::make_unique<int>();
 
  std::unique_ptr<int> err(a); // compilation error
  a = b;                       // compilation error
}
Если же нужно создать новые указатели и присвоить по этим адресам  значения объектов уже существующих, можете поступать так как в примере ниже.
void foo_a()
{
  struct cls
  {
    cls()           
    { 
      cout << "cls()" << endl;
    }
    cls(const cls&) 
    { 
      cout << "cls(const cls&)" << endl;
    }
    ~cls()          
    { 
      cout << "~cls()" << endl;
    }
  };
 
  auto a = std::make_unique<cls>();
  auto b = std::make_unique<cls>();
 
  // конструируем объект unique_ptr на основе 
  // уже существующего объекта 'a'
  // выделяем память с помощью оператора new 
  // и инициализируем
  // у объекта 'a' и 'c' адреса указателей разные
  // (уникальны, и у каждого свой владелец)
  // но одинаковы значения
  // (работает конструктор копирования) 
  // расположенные по этим адресам
  std::unique_ptr<cls> c(new cls(*a));     // OK
 
  // почти тоже самое, так как объект 'c' уже 
  // что-то содержит, то это что-то будет 
  // уничтожено а дальше будет создан новый 
  // объект и 'с' будет содержать его адрес, 
  // по которому будут скопированы
  // (работает конструктор копирования) 
  // данные объекта 'b'
  c = std::make_unique<cls>(*b);          // OK
}
Мы уже знаем что конструкторов копирования и операторов присваивания у unique_ptr нет, значит мы должны понимать что код ниже собираться не будет.
void foo_a()
{
  struct cls
  {
    cls()  { cout << "cls()" << endl;}
    ~cls() { cout << "~cls()" << endl;}
 
    std::unique_ptr<int> mupi_a;
  };
 
  cls a;
  cls b;
  cls c(a);       // compilation error
  a = b;          // compilation error
}
Так как компилятор, для класса cls автоматически сгенерит конструктор копирования и оператор присваивания в которых будет попытка сконструировать(или переприсвоить) член класса mupi_a тип у которого std::unique_ptr другим объектом unique_ptr, что ЗАПРЕЩЕНО!Значит нужно что? Правильно - описать свой конструктор копирования и оператор присваивания + поступить как мы поступили выше, а именно: 
void foo_b()
{
  struct cls
  {
    cls()
    {
      cout << "cls()" << endl;
    }
    cls(const cls &aObj)
      :mp(aObj.mp 
        ? new int(*(aObj.mp)) 
        : nullptr)
    {
      cout << "cls(const cls&)" << endl;
    }
    cls &operator=(const cls &aRhs)
    {
      mp = aRhs.mp 
         ? std::make_unique<int>(*(aRhs.mp)) 
         : nullptr;
      cout << "operator = " << endl; return *this;
    }
    ~cls()
    {
      cout << "~cls()" << endl;
    }
    std::unique_ptr<int> mp;
  };
 
  cls a;
  cls b;
  cls c(a);       // OK
  a = b;          // OK
}
!Но! Это отчасти можно назвать костылём(все зависит от задачи), потому что unique_ptr уникальный указатель с одним владельцем, если вам нужно совместное владение, то скорее всего имеет смысл посмотреть в сторону shared_ptr(ниже есть описание), но если вы туда уже смотрели, и вас не устраивает, к примеру то что размер объекта shared_ptr как минимум в два раза больше чем стандартный указатель, может быть вас не устроят какие-то затраты производительности, может быть еще что-то, тогда да, возможно описанное выше решение вполне подойдет для вашей задачи.
Если вдруг нужен массив уникальных указателей, вместо стандартных массивов С++, используйте код ниже.
    auto spa = std::unique_ptr<int[]>(new int [3]);
    spa.get()[0] = 1;
    spa.get()[1] = 2;
    spa.get()[2] = 3;

    std::cout << spa.get()[0]
              << ","
              << spa.get()[1]
              << ","
              << spa.get()[2]
              << std::endl;

Итог
up -  объект unique_ptr
p   - указатель
  • unique_ptr up {};       - Конструктор по умолчанию, up содержит nullptr
  • unique_ptr up {p};     - up содержит p
  • unique_ptr up {up2}; - Перемещающий конструктор, up содержит p из up2, up2 получает nullptr
  • up.~unique_ptr();      - Удаление указателя up
  • p = up.get();              - p указатель содержащийся в up
  • p = up.release();        - p указатель содержавшийся в up, после вызова up.release() содержит nullptr
  • up.reset(p);               - Удаление указателя, содержавшегося в up, после вызова up.reset(p) содержит p
  • up=make_unique<X> (args); - up содержит new<X>(args)

 

std::shared_ptr


 shared_ptr - позволяет совместно владеть указателем, то есть может существовать много объектов shared_ptr указывающих на один и тот же объект. Объект удаляется при уничтожении последнего владельца.
void foo_a()
{
  std::shared_ptr<int> sp_a(new int(1));
 
  cout << "sp_a = " 
       << *sp_a 
       << "(" 
       << sp_a.get() 
       << ")" 
       << endl;
  {
    std::shared_ptr<int> sp_b(sp_a);
    cout << "sp_b = " 
         << *sp_b 
         << "(" 
         << sp_b.get() 
         << ")" 
         << endl;
  }
}
В примере выше, мы создаем указатель sp_a, далее, создаем на основе указателя sp_a, второй указатель sp_b. Когда объект sp_b покидает свою область видимости, ТО! объект, указатель на который хранит sp_a и sp_b, не уничтожается, уничтожается он только тогда когда sp_a покидает свою область видимости.
При совместном владении одним указателем, объекты shared_ptr хранят счетчик ссылок, объект уничтожается только тогда когда счетчик становится равным 0 (означает что на этот объект больше никто не ссылается), а это одно из правил работы сборщика мусора в других языках программирования(для которых этот сборщик есть). Конструкторы shared_ptr увеличивают этот счетчик, а деструкторы уменьшают. Чтобы узнать количество владельцев, нужно вызвать функцию use_count(), которая возвращает количество владельцев. Смотрим пример ниже.
void foo_a()
{
  std::shared_ptr<int> sp_a(new int(1));
  cout << "count of owners= " 
       << sp_a.use_count() 
       << endl;                     // 1
  {
    std::shared_ptr<int> sp_b(sp_a);
    cout << "count of owners= " 
         << sp_a.use_count() 
         << endl;                   // 2
    {
      std::shared_ptr<int> sp_c;
      sp_c = sp_b;
      cout << "count of owners= " 
           << sp_a.use_count() 
           << endl;                 // 3
    }
    cout << "count of owners= " 
         << sp_a.use_count() 
         << endl;                   // 2
  }
  cout << "count of owners= " 
       << sp_a.use_count() 
       << endl;                     // 1
}
Это всё, немного влияет на производительность.Размер std::shared_ptr в два раза больше размера обычного указателя, потому что этот объект shared_ptr содержит указатель на объект которым владеет, и указатель на счетчик ссылок(Скотт Мейерс пишет что стандарт не требует такой реализации, но большинство реализаций реализуют это именно так).
Память для счетчика ссылок должна выделяться автоматически.
Увеличение и уменьшение счетчика ссылок должно быть атомарным.

Точно так же как и для unique_ptr для sharde_ptr есть функции reset() и swap(), но нет функции release().
void foo_a()
{
  struct cls
  {
    cls()  { cout << "cls()" << endl;}
    ~cls() { cout << "~cls()" << endl;}
  };
 
  // вызывается конструктор, 
  // счетчик ссылок равен 1
  std::shared_ptr<cls> sp_a(new cls);
 
  // инициализация нового объекта, 
  // на основе существующего
  // с совместным владением одним указателем
  // счетчик ссылок увеличивается на 1, 
  // и равен 2
  std::shared_ptr<cls> sp_b(sp_a);
 
  // счетчик ссылок уменьшается на 1, 
  // и равен 1
  // значит объект не уничтожается
  sp_b.reset();
 
  // счетчик ссылок уменьшается на 1, 
  // и равен 0
  // значит объект уничтожается, для него 
  // вызывается стандартный деструктор
  sp_a.reset();
}
Единственный конструктор который не увеличивает счетчик ссылок, это перемещающий конструктор(и перемещающее присваивание), потому что он перемещает объект, а не передает в совместное пользование. Перемещающее конструирование указателя из одного shared_ptr в другой, делает исходный указатель нулевым, то есть старый указатель перестает указывать на объект, а новый начинает это делать, поэтому изменение счетчика не требуется.
void foo_a()
{
  std::shared_ptr<int> sp_a(new int(1));
  std::shared_ptr<int> sp_b(std::move(sp_a));
  // после конструирования перемещением объекта sp_a 
  // в объект sp_b
  // sp_a == nullptr, счетчик ссылок = 0
  // sp_b != nullptr, счетчик ссылок = 1
}
Есть интересная функция, которая, как я понял сравнивает адреса управляющих блоков (в своей книге - Эффективный и современный С++. Скотт Мейерс подробно описывает что такое управляющий блок, то есть то что контролирует количество ссылок, описывать это здесь не буду, так как это будет дублирование части книги), это функция owner_before(), но какова ее польза, и ее применение, на данный момент я не понял. Когда я разберусь в нюансах ее использования, я обновлю этот блог. Ниже пару эксперементов с этой функцией.
void foo_a()
{
  std::shared_ptr<int> sp_a(new int(1));
  std::shared_ptr<int> sp_b(new int(2));
 
  std::shared_ptr<int> sp_c(sp_a);
 
  bool res{false};
 
  // true - упр. блок для sp_a был 
  // создан раньше чем для sp_b
  res = sp_a.owner_before(sp_b);
 
  // false - упр. блок для sp_b был 
  // создан позже чем для sp_a
  res = sp_b.owner_before(sp_a);
 
  // true - упр. блок для sp_c был 
  // создан раньше
  // (совместное владение с sp_a) 
  // чем для sp_b
  res = sp_c.owner_before(sp_b);
 
  // false - упр. блок для sp_b был 
  // создан позже чем для sp_с 
  // (совместное владение с sp_a)
  res = sp_b.owner_before(sp_c);
 
  // false - нет объяснения, возможно 
  // потому что для sp_c нет управляющего 
  // блока, и он есть только для sp_a
  res = sp_a.owner_before(sp_c);
  res = sp_c.owner_before(sp_a);
}
Для shared_ptr можно также указать свои удаляторы, аналогично тому как это делается в unique_ptr. Скотт Мейерс не рекомендует создавать указатели shared_ptr из переменных тип которых обычный указатель. Я полность поддерживаю это.
Ниже пример того чего следует избегать!
void foo_a()
{
  {
    auto *pInt = new int(1);
    
    // создается один управляющий блок
    std::shared_ptr<int> sp_a(pInt); 
    
    // создается второй управляющий блок
    std::shared_ptr<int> sp_b(pInt); 
 
    // для не интеллектуального указателя 
    // было создано два управляющих блока!
    // были потрачены ресурсы, время, 
    // и все это для чего? ...
    delete pInt;
  }
 
  // !!A для того!!! чтобы получить 
  // аварийное завершение нашей программы,
  // до этого коментария программа не доживет
}
Лучше это делать так:
void foo_a()
{
  // создается один управляющий блок
  std::shared_ptr<int> sp_a(new int(1)); 
  // совместное владение
  std::shared_ptr<int> sp_b(sp_a);        
}
Из объектов unique_ptr легко делать объекты shared_ptr
void foo_a()
{
  int i = 0;
  std::unique_ptr<int> up_a(new int(1));
  std::shared_ptr<int> sp_a(std::move(up_a));
 
  // после конструирования объекта shared_ptr 
  // перемещением unique_ptr,
  // объект unique_ptr будет перемещен в объект 
  // shared_ptr,
  // после чего он unique_ptr будет равен nullptr
 
  // это условие никогда не выполнится :-(
  // исключением будет, если у вас
  // неправильный компилятор, и он генерит 
  // неправильный код
  if(up_a)
    std::cout << "I am LIVE! I was not moved!" 
              << std::endl;
}

Создание массивов(но лучше использовать стандартные array, vector, ...) немного отличается от способа который используется для unique_ptr, для коректной работы при создании массива, нужно обязательно указать способ удаления, удалятор, в примере ниже показано 3 способа создание массивов, но вы можете придумать свои способы :-)
void foo_a()
{
  {
    auto spa = std::shared_ptr<int>(new int [3], default_delete<int[]>());
} {
    auto spa = std::shared_ptr<int>(new int [3], []( int *p ) { delete[] p; });
} {
    struct IntArrayDeleter
    {
        void operator()(int *aP)
        {
          std::cout <<
                    "deleted is OK"
                    << std::endl;
          delete [] aP;
        }
    };
    auto spa = std::shared_ptr<int>(new int [3], IntArrayDeleter());
  }
}

Итог
sp -  объект shared_ptr
p   - указатель
  • shared_ptr up {};       - Конструктор по умолчанию, sp содержит nullptr
  • shared_ptr sp {p};      - sp содержит p
  • shared_ptr sp {sp2};   - Копирующий конструктор, и sp и sp2 содержат p из sp2
  • shared_ptr sp {move(sp2)} - Перемещающий конструктор, sp содержит p из sp2, sp2 содержит nullptr
  • sp.~shared_ptr();      - Удаление указателя содержащегося в sp, если sp является последним shared_ptr для этого указателя
  • sp = sp2                   - Копирующие присваивание, если sp является последним общим указателем, содержащим данный указатель p, этот указатель удаляется, после присваивания и sp, и sp2 содержат указатель p из sp2.
  • sp = move(sp2)        - Перемещающе присваивание, если sp является последним общим указателем, содержащим данный указатель p, этот указатель удаляется, после присваивания sp содержит указатель p из sp2, а sp2 содержит nullptr
  • p = sp.get()              - p указатель, содержащийся в sp
  • n = sp.use_count()   - количество владельцев указателя содержащегося в sp
  • sp.reset(p)               - если sp является последним общим указателем, содержащим некоторый указатель, этот указатель удаляется, sp содержит p
  • sp=make_shared<X> (args); - sp содержит new<X>(args)


std::weak_ptr


weak_ptr по факту интеллектуальным указателем не является, этот тип указателя создан для разрушения циклических зависимостей, которые могут образоваться при использовании shared_ptr. Объекты weak_ptr, так же как и объекты shared_ptr имеют указатель на счетчик ссылок, его еще могут называть слабый счетчик ссылок, то есть объекты weak_ptr хранят слабые ссылки, а shared_ptr хранят сильные ссылки. С объектами со слабыми ссылками можно работать так же как и с объектами с сильными ссылками, но при необходимости объект будет удален, даже если на его есть слабая ссылка. Счетчик ссылок weak_ptr не влияет на счетчик ссылок shared_ptr. Смотрите примеры ниже, с комментариями:
struct sUnit
{
  sUnit()  { cout << "sUnit()" << endl;  }
  ~sUnit() { cout << "~sUnit()" << endl; }

  shared_ptr<sUnit> unit;
};

int main()
{
  shared_ptr<sUnit> *ref = nullptr;
  {
    shared_ptr<sUnit> a = make_shared<sUnit>();
    // счетчик сильных ссылок равен 1
    cout << a.use_count() << endl << endl;

    ref = &a;

    a.get()->unit = a;
    // счетчик сильных ссылок равен 2
    cout << a.use_count() << endl;
    cout << a.get()->unit.use_count() << endl << endl;
  }

  // счетчик сильных ссылок остался  равен 1,
  // при выходе из области видимости деструктор для
  // объекта shared_ptr был вызван один раз
  // объекты потеряны, но продолжают 'висеть' в памяти
  cout << ref->use_count() << endl;
  cout << ref->get()->unit.use_count() << endl << endl;

  return 0;
}
меняем тип члена класса sUnit::unit с shared_ptr на weak_ptr.
struct sUnit
{
  sUnit()  { cout << "sUnit()" << endl;  }
  ~sUnit() { cout << "~sUnit()" << endl; }
  weak_ptr<sUnit> unit;
};

int main()
{
  shared_ptr<sUnit> *ref = nullptr;
  {
    shared_ptr<sUnit> a = make_shared<sUnit>();
    // счетчик сильных ссылок равен 1
    cout << a.use_count() << endl << endl;

    ref = &a;

    a.get()->unit = a;
    // счетчик сильных ссылок равен 1
    // счетчик слабых ссылок равен 1
    cout << a.use_count() << endl;
    cout << a.get()->unit.use_count() << endl << endl;
  }

  // при выходе из области видимости деструктор для
  // объекта shared_ptr был вызван один раз, счетчик
  // сильных ссылок уменьшился до 0, объект был удален.
  // тот факт что еще была еще таблица со слабыми ссылками
  // никак не влияет на то что объект не будет уничтожен
  cout << ref->use_count() << endl;
  cout << ref->get()->unit.use_count() << endl << endl;

  return 0;
}
Кроме того, из слабой ссылки, можно сделать сильную, что иногда нужно. Для этого у объектов weak_ptr есть функция lock().
struct sUnit
{
  sUnit()  { cout << "sUnit()" << endl;  }
  ~sUnit() { cout << "~sUnit()" << endl; }
};

int main()
{
  shared_ptr<sUnit> a = make_shared<sUnit>();
  shared_ptr<sUnit> b = a;
  weak_ptr<sUnit>   c = b;

  // счетчик сильных ссылок равен 2
  // счетчик слабых ссылок равен 2
  cout << a.use_count() << ":"
       << b.use_count() << ":"
       << c.use_count() << endl << endl;

  a.reset();
  // счетчик ссылок для объекта 'a' равен 0
  // счетчик сильных ссылок равен 1
  // счетчик слабых ссылок равен 1
  // объект 'a' равен nullptr
  cout << a.use_count() << ":"
       << b.use_count() << ":"
       << c.use_count() << endl << endl;

  // при необходимости мы можем из слабой ссылки
  // сделать сильную
  a = c.lock();
  // счетчик ссылок для объекта 'a' равен 1
  // счетчик сильных ссылок равен 2
  // счетчик слабых ссылок равен 2
  cout << a.use_count() << ":"
       << b.use_count() << ":"
       << c.use_count() << endl << endl;

  a.reset();
  // счетчик сильных ссылок равен 1
  // счетчик слабых ссылок равен 1
  cout << a.use_count() << ":"
       << b.use_count() << ":"
       << c.use_count() << endl << endl;

  b.reset();

  // счетчик сильных ссылок равен 0
  // счетчик слабых ссылок равен 0
  cout << a.use_count() << ":"
       << b.use_count() << ":"
       << c.use_count() << endl << endl;

  return 0;
}
!ВАЖНО! Перед тем как обращаться к объекту shared_ptr посредством объекта weak_ptr, нужно обязательно проверить на то, жив ли этот объект shared_ptr или нет, для этого у объекта weak_ptr есть функция expired(), которая возвращает false если объекта shared_ptr уже нет в живых.
struct sUnit
{
  sUnit()  { cout << "sUnit()" << endl;  }
  ~sUnit() { cout << "~sUnit()" << endl; }
  int health{100};
};

int main()
{
  shared_ptr<sUnit> a = make_shared<sUnit>();
  weak_ptr<sUnit>   b = a;

  if(!b.expired())
    cout << "healt = " << b.lock().get()->health << endl;
  else
    cout << "object of sUnit was deleted" << endl;

  a.reset();

  if(!b.expired())
    cout << "healt = " << b.lock().get()->health << endl;
  else
    cout << "object of sUnit was deleted" << endl;

  return 0;
}
Как вы уже поняли, прямого доступа к объекту shared_ptr из объекта weak_ptr нет, для этого нужно использовать функцию lock()(которая генерит объект shared_ptr) в связке с функцией expired(), чтобы случайно не обратиться к мертвому указателю.
Ниже еще пару примеров известных структур данных, где будет циклическая зависимость, но используется weak_ptr для разрушения этих зависимостей.
struct sTreeNode
{
  int value;
  shared_ptr<sTreeNode> child;
  weak_ptr<sTreeNode>   parent;
};

struct sList
{
  int value;
  shared_ptr<sList> next;
  weak_ptr<sList>   prev;
};


Предпочитайте использование std::make_unique и std::make_shared непосредственному использованию оператора new.

Из книги Скотта Мейерса: 
  auto upw1(std::make_unique<Widget>());
  std::unique_ptr<Widget> upw2(new Widget);

  auto spw1(std::make_shared<Widget>());
  std::shared_ptr<Widget> spw2(new Widget);
Повторение типа(с оператором new мы дублируем имя типа) идет вразрез с основным принципом разработки программного обеспечения: избегать дублирования кода. Дублирование в исходном тексте увеличивает время компиляции, может вести к раздутому объектному коду и в общем случае приводит к коду, с которым сложно работать. Зачастую ведет к несогласованному коду, а несогласованность часто ведет к ошибкам.
Вторая причина это безопасность исключений.
int f_a();
void calc(shared_ptr<sUnit>, int);

// ...

// при вызове calc()
// потенциальная утечка памяти
calc(shared_ptr<sUnit>(new sUnit), f_a());
Дело в том что, мы не можем знать какой код сгенерит компилятор, все что мы знаем, так это то что до вызова функции сначала происходит вычисление всех аргументов, какой порядок мы не знаем. В этом примере код может быть сгенерирован следующим образом:

  1. Выполнить new sUnit
  2. Выполнить f_a()
  3. Вызвать конструктор shared_ptr
Если при выполнении функции f_a происходит исключительная ситуация(генериться исключение, которое выше по стэку вызовов перехватывается), то до работы конструктора shared_ptr не доходит, получается на первом шаге, память будет выделена, но не будет освобождена, объект останется в памяти без возможности доступа, что является утечкой памяти.
Соответственно безопаснее использовать функции make_shared, make_unique.
int f_a();
void calc(shared_ptr<sUnit>, int);

// ...

calc(make_shared<sUnit>(), f_a());
В этом случае либо make_shared либо f_a() будет вызвана первой, если make_shared указатель будет создан в безопасном месте, если же f_a() то до make_shared дело даже не дойдет, поэтому в плане утечки памяти тут все безопасно.
О других рекомендациях читайте у Скотта Мейерса, чтобы я тут не перепечатывал.

В блоге возможны дополнения!






Комментарии

  1. Я думаю, у вас есть отличная статья, но позвольте мне поделиться со всеми вами здесь о моем опыте работы с кредитором по имени Бенджамин Ли, который помог мне расширить мой бизнес с помощью своей кредитной компании, которая предложила мне сумму кредита в размере 600000,00 долларов США, которую я использовал раньше обновить мой бизнес несколько месяцев назад. Ему было действительно здорово работать с ним, потому что он Нежный человек с добрым сердцем, человек, который может слушать ваше сердцебиение и говорить вам, что все будет хорошо, когда я связался с мистером Ли, на моей странице в Facebook появилась его реклама. Затем я посетил его офис в Мичигане, чтобы обсудить кредитное предложение, которое он и его компания предоставляют. Он дает мне понять, как проходит весь процесс, и я решил попробовать, чтобы оно было успешным, как он и обещал, да, я ему верю, Я доверяю ему, я полагаюсь на него также во всех моих проектах, он будет моим дорогим финансовым директором, и я рад, что мой бизнес, вероятно, идет хорошо, и я собираюсь сделать мой бизнес с его помощью расти, как трава. Он работает с отличные инвесторы и знаете что? Они также выдают международные займы. Разве это не здорово слышать, когда вы знаете, что с каждым днем ​​растет множество бизнес-проектов, в вашем сердце надеясь, что вы собираетесь получать доход от этой работы, чтобы собрать деньги для проекта, Ops, тогда мистер Ли поможет вам с что, да, международная ссуда, он поможет вам в этом отлично, потому что я очень доверяю ему в такой работе. Смотри, не стесняйся и не затушевывай, возможно, попробуй мистеру Ли здесь его контакт: 247officedept@gmail.com

    ОтветитьУдалить

Отправить комментарий