Шрифт:
Интервал:
Закладка:
class Foo
{
string name;
Foo(string n)
: name{n}
{ cout << "CTOR " << name << 'n'; }
~Foo() { cout << "DTOR " << name << 'n';}
3. Статические методы create_foo и destroy_foo будут создавать и удалять экземпляры типа Foo. Они работают с необработанными указателями. Это симулирует ситуацию, возникающую при использовании устаревшего API языка С, который не дает задействовать их непосредственно для обычных указателей shared_ptr:
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<Foo> make_shared_foo(string s)
{
return {Foo::create_foo(move(s)), Foo::destroy_foo};
}
5. Обратите внимание: make_shared_foo возвращает обычный экземпляр shared_ptr<Foo>, поскольку передача пользовательской функции удаления не изменяет ее тип. Это произошло потому, что shared_ptr применяет вызовы виртуальных функций для сокрытия таких деталей. Уникальные указатели не создают наличных издержек; это не дает задействовать для них подобный прием. Нужно изменить тип unique_ptr. В качестве второго шаблонного параметра мы передадим экземпляр типа void (*)(Foo*), данный тип имеет и указатель на функцию destroy_foo:
static unique_ptr<Foo, void (*)(Foo*)> 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<Foo, void (*)(Foo*)>. Теперь он может хранить указатель на функцию destroy_foo, которую мы предоставляем в качестве второго параметра конструктора в нашей функции make_unique_foo.
Если передача пользовательской функции удаления для класса unique_ptr заставила нас сменить его тип, то почему же мы смогли сделать то же самое для shared_ptr, не изменяя его тип? Единственное, что нам пришлось сделать, — передать второй параметр для конструктора shared_ptr. Почему это не может быть так же просто и для типа unique_ptr?
Почему так просто передать экземпляру класса shared_ptr некоторый вызываемый объект delete, не изменяя типа общего указателя? Причина кроется в природе общих указателей, поддерживающих блок управления. Блок управления общих указателей — объект, имеющий виртуальные функции. Это значит, что блок управления обычного общего указателя и блок управления общего указателя с пользовательским delete различаются! Чтобы с помощью уникального указателя применить пользовательскую функцию удаления, нужно изменить тип этого указателя. Если мы хотим, чтобы общий указатель задействовал пользовательскую функцию удаления, то это также изменит тип внутреннего блока управления, невидимого для нас, поскольку данная разница скрыта за интерфейсом виртуальной функции.
Описанный прием можно применить и для уникальных указателей, но в таком случае он повлечет некоторые издержки во время выполнения программы. Это не то, что мы хотим, поскольку уникальные указатели не должны создавать лишних издержек.
Открываем доступ к разным переменным — членам одного объекта
Представим, что у нас есть общий указатель на некий сложный объект, память для которого выделяется динамически. Нужно создать новый поток, выполняющий какую-то продолжительную работу для одного из членов этого сложного объекта. Если мы хотим освободить этот общий указатель сейчас, то объект будет удален, хотя другие потоки все еще могут пытаться получить к нему доступ. Если же мы не хотим давать объекту потока указатель на весь объект, поскольку данное действие пересечется с нашим «аккуратным» интерфейсом или по каким-то другим причинам, то значит ли это, что придется управлять памятью вручную?
Нет. Вы можете использовать общие указатели, которые, с одной стороны, ссылаются на член крупного общего объекта, а с другой — выполняют автоматическое управление памятью для всего исходного объекта.
В данном разделе мы создадим подобный сценарий (без потоков, чтобы не усложнять задачу) с целью ознакомиться с этой удобной функцией типа shared_ptr.
Как это делается
В этом примере мы определим структуру, которая состоит из нескольких членов. Далее выделим память для экземпляра структуры в куче, ее будет сопровождать общий указатель. Из него мы получим больше общих указателей, указывающих не на сам объект, а на его члены.
1. Сначала включим необходимые заголовочные файлы, а затем объявим об использовании пространства имен std по умолчанию:
#include <iostream>
#include <memory>
#include <string>
using namespace std;
2. Далее определим класс, который имеет разные члены. Позволим общим указателям указывать на