C++17 STL Стандартная библиотека шаблонов — страница 91 из 119

shared_p
tr:


public:

  static Foo* create_foo(string s) {

    return new Foo{move(s)};

  }

  static void destroy_foo(Foo *p) { delete p; }

};


4. Теперь сделаем так, чтобы подобными объектами можно было управлять с помощью

shared_ptr
. Конечно, можно помещать указатели, которые получаем из функции
create_foo
, в конструктор общего указателя. Разрушение объекта выглядит сложнее, поскольку функция удаления класса
shared_ptr
, использующаяся по умолчанию, решит проблему неправильно. Идея заключается в том, что можно задать для класса
shared_ptr
пользовательскую функцию удаления. Сигнатура функции, которую следует иметь функции удаления или вызываемому объекту, должна совпадать с сигнатурой функции
destroy_foo
. Если функция, которую нужно вызвать для разрушения объекта, более сложна, то можно обернуть ее в лямбда-выражение.


static shared_ptr make_shared_foo(string s)

{

  return {Foo::create_foo(move(s)), Foo::destroy_foo};

}


5. Обратите внимание:

make_shared_foo
возвращает обычный экземпляр
shared_ptr
, поскольку передача пользовательской функции удаления не изменяет ее тип. Это произошло потому, что
shared_ptr
применяет вызовы виртуальных функций для сокрытия таких деталей. Уникальные указатели не создают наличных издержек; это не дает задействовать для них подобный прием. Нужно изменить тип
unique_ptr
. В качестве второго шаблонного параметра мы передадим экземпляр типа
void (*)(Foo*)
, данный тип имеет и указатель на функцию
destroy_foo
:


static unique_ptr make_unique_foo(string s)

{

  return {Foo::create_foo(move(s)), Foo::destroy_foo};

}


6. В функции

main
просто создаем экземпляры общего и уникального указателей. В выходных данных программы увидим, будут ли они уничтожаться корректно и автоматически.


int main()

{

  auto ps (make_shared_foo("shared Foo instance"));

  auto pu (make_unique_foo("unique Foo instance"));

}


7. Компиляция и запуск программы дадут ожидаемый результат:


$ ./legacy_shared_ptr

CTOR shared Foo instance

CTOR unique Foo instance

DTOR unique Foo instance

DTOR shared Foo instance


Как это работает

Обычно

unique_ptr
и
shared_ptr
просто вызывают оператор
delete
для внутренних указателей, когда должны уничтожить объект, который сопровождают. В этом разделе мы создали класс, для которого нельзя выделить память, используя
x = new Foo{123}
, и разрушить объект непосредственно с помощью
delete x
.

Функция

Foo::create_foo
просто возвращает необработанный указатель на только что созданный экземпляр типа
Foo
, и это не вызывает других проблем, поскольку умные указатели работают с необработанными.

Сложность заключается в том, что нужно научить классы

unique_ptr
и
shared_ptr
разрушать объект, если способ по умолчанию не подходит.

С этой точки зрения оба типа умных указателей несколько отличаются друг от друга. Чтобы определить пользовательскую функцию удаления для

unique_ptr
, нужно изменить его тип. Поскольку тип сигнатуры
delete
класса
Foo
void Foo::destroy_foo(Foo*);
, типом уникального указателя, сопровождающего экземпляр типа
Foo
, должен быть
unique_ptr
. Теперь он может хранить указатель на функцию
destroy_foo
, которую мы предоставляем в качестве второго параметра конструктора в нашей функции
make_unique_foo
.

Если передача пользовательской функции удаления для класса

unique_ptr
заставила нас сменить его тип, то почему же мы смогли сделать то же самое для
shared_ptr
, не изменяя его тип? Единственное, что нам пришлось сделать, — передать второй параметр для конструктора
shared_ptr
. Почему это не может быть так же просто и для типа
unique_ptr
?

Почему так просто передать экземпляру класса

shared_ptr
некоторый вызываемый объект
delete
, не изменяя типа общего указателя? Причина кроется в природе общих указателей, поддерживающих блок управления. Блок управления общих указателей — объект, имеющий виртуальные функции. Это значит, что блок управления обычного общего указателя и блок управления общего указателя с пользовательским
delete
различаются! Чтобы с помощью уникального указателя применить пользовательскую функцию удаления, нужно изменить тип этого указателя. Если мы хотим, чтобы общий указатель задействовал пользовательскую функцию удаления, то это также изменит тип внутреннего блока управления, невидимого для нас, поскольку данная разница скрыта за интерфейсом виртуальной функции.

Описанный прием можно применить и для уникальных указателей, но в таком случае он повлечет некоторые издержки во время выполнения программы. Это не то, что мы хотим, поскольку уникальные указатели не должны создавать лишних издержек. 

Открываем доступ к разным переменным — членам одного объекта

 Представим, что у нас есть общий указатель на некий сложный объект, память для которого выделяется динамически. Нужно создать новый поток, выполняющий какую-то продолжительную работу для одного из членов этого сложного объекта. Если мы хотим освободить этот общий указатель сейчас, то объект будет удален, хотя другие потоки все еще могут пытаться получить к нему доступ. Если же мы не хотим давать объекту потока указатель на весь объект, поскольку данное действие пересечется с нашим «аккуратным» интерфейсом или по каким-то другим причинам, то значит ли это, что придется управлять памятью вручную?

Нет. Вы можете использовать общие указатели, которые, с одной стороны, ссылаются на член крупного общего объекта, а с другой — выполняют автоматическое управление памятью для всего исходного объекта.

В данном разделе мы создадим подобный сценарий (без потоков, чтобы не усложнять задачу) с целью ознакомиться с этой удобной функцией типа

shared_ptr
.


Как это делается

В этом примере мы определим структуру, которая состоит из нескольких членов. Далее выделим память для экземпляра структуры в куче, ее будет сопровождать общий указатель. Из него мы получим больше общих указателей, указывающих не на сам объект, а на его члены.


1. Сначала включим необходимые заголовочные файлы, а затем объявим об использовании пространства имен

std
по умолчанию:


#include 

#include 

#include 


using namespace std;


2. Далее определим класс, который имеет разные члены. Позволим общим указателям указывать на отдельные члены. Чтобы увидеть, когда класс создается и уничтожается, будет выводить сообщения на экран в конструкторе и деструкторе.


struct person {

  string name;

  size_t age;

  person(string n, size_t a)

    : name{move(n)}, age{a}

  { cout << "CTOR " << name << '\n'; }

  ~person() { cout << "DTOR " << name << '\n'; }

};


3. Определим общие указатели, которые имеют корректные типы, чтобы указывать на переменные-члены

name
и
age
экземпляра класса
person
:


int main()