12.1.3. Использование указателя shared_ptr с оператором new
Как уже упоминалось, если не инициализировать интеллектуальный указатель, он инициализируется как нулевой. Как свидетельствует табл. 12.3, интеллектуальный указатель можно также инициализировать указателем, возвращенным оператором new:
shared_ptr<double> p1; // shared_ptr может указывать на double
shared_ptr<int> p2(new int(42)); // p2 указывает на int со значением 42
Конструкторы интеллектуального указателя, получающие указатели, являются явными (см. раздел 7.5.4). Следовательно, нельзя неявно преобразовать встроенный указатель в интеллектуальный; для инициализации интеллектуального указателя придется использовать прямую форму инициализации (см. раздел 3.2.1):
shared_ptr<int> p1 = new int(1024); // ошибка: нужна
// прямая инициализация
shared_ptr<int> p2(new int(1024)); // ok: использует
// прямую инициализацию
Таблица 12.3. Другие способы определения и изменения указателя shared_ptr
shared_ptr<T> p(q) Указатель p управляет объектом, на который указывает указатель встроенного типа q; указатель q должен указывать на область памяти, зарезервированную оператором new, а его тип должен быть преобразуем в тип Т* shared_ptr<T> p(u) Указатель p учитывает собственность указателя u типа unique_ptr; указатель u становится нулевым shared_ptr<T> p(q, d) Указатель p учитывает собственность объекта, на который указывает встроенный указатель q. Тип указателя q должен быть преобразуем в тип Т* (см. раздел 4.11.2). Для освобождения q указатель p будет использовать вызываемый объект d (см. раздел 10.3.2) вместо оператора delete shared_ptr<T> p(p2, d) Указатель p — это копия указателя p2 типа shared_ptr, как описано в табл. 12.2, за исключением того, что указатель p использует вызываемый объект d вместо оператора delete p.reset() p.reset(q) p.reset(q, d) Если p единственный указатель shared_ptr на объект, функция reset() освободит существующий объект p. Если передан необязательный встроенный указатель q, то p будет указывать на q, в противном случае p станет нулевым. Если предоставлен вызываемый объект d, то он будет вызван для освобождения указателя q, в противном случае используется оператор deleteИнициализация указателя p1 неявно требует, чтобы компилятор создал указатель типа shared_ptr из указателя int*, возвращенного оператором new. Поскольку нельзя неявно преобразовать обычный указатель в интеллектуальный, такая инициализация ошибочна. По той же причине функция, возвращающая указатель shared_ptr, не может неявно преобразовать простой указатель в своем операторе return:
shared_ptr<int> clone(int p) {
return new int(p); // ошибка: неявное преобразование
// в shared_ptr<int>
}
Следует явно связать указатель shared_ptr с указателем, который предстоит возвратить:
shared_ptr<int> clone (int p) {
// ok: явное создание shared_ptr<int> из int*
return shared_ptr<int>(new int(p));
}
По умолчанию указатель, используемый для инициализации интеллектуального указателя, должен указывать на область динамической памяти, поскольку по умолчанию интеллектуальные указатели используют оператор delete для освобождения связанного с ним объекта. Интеллектуальные указатели можно связать с указателями на другие виды ресурсов. Но для этого необходимо предоставить собственную функцию, используемую вместо оператора delete. Предоставление собственного кода удаления рассматривается в разделе 12.1.4.
Указатель shared_ptr может координировать удаление только с другими указателями shared_ptr, которые являются его копиями. Действительно, этот факт — одна из причин, по которой рекомендуется использовать функцию make_shared(), а не оператор new. Это связывает указатель shared_ptr с объектом одновременно с его резервированием. При этом нет никакого способа по неосторожности связать ту же область памяти с несколькими независимо созданными указателями shared_ptr.
Рассмотрим следующую функцию, работающую с указателем shared_ptr:
// ptr создается и инициализируется при вызове process()
void process(shared_ptr<int> ptr) {
// использование ptr
} // ptr выходит из области видимости и удаляется
Параметр функции process() передается по значению, поэтому аргумент копируется в параметр ptr. Копирование указателя shared_ptr осуществляет инкремент его счетчика ссылок. Таким образом, в функции process() значение счетчика не меньше 2. По завершении функции process() осуществляется декремент счетчика ссылок указателя ptr, но он не может достигнуть нуля. Поэтому, когда локальная переменная ptr удаляется, память, на которую она указывает, не освобождается.
Правильный способ использования этой функции подразумевает передачу ей указателя shared_ptr:
shared_ptr<int> p(new int (42)); // счетчик ссылок = 1
process(p); // копирование p увеличивает счетчик;
// в функции process() счетчик = 2
int i = *p; // ok: счетчик ссылок = 1
Хотя функции process() нельзя передать встроенный указатель, ей можно передать временный указатель shared_ptr, явно созданный из встроенного указателя. Но это, вероятно, будет ошибкой:
int *x(new int(1024)); // опасно: x - обычный указатель, a
// не интеллектуальный process(x);
// ошибка: нельзя преобразовать int* в shared_ptr<int>
process(shared_ptr<int>(x)); // допустимо, но память будет освобождена!
int j = *x; // непредсказуемо: x - потерянный указатель!
В этом вызове функции process() передан временный указатель shared_ptr. Этот временный указатель удаляется, когда завершается выражение, в котором присутствует вызов. Удаление временного объекта приводит к декременту счетчика ссылок, доводя его до нуля. Память, на которую указывает временный указатель, освобождается при удалении временного указателя.
Но указатель x продолжает указывать на эту (освобожденную) область памяти; теперь x — потерянный указатель. Результат попытки использования значения, на которое указывает указатель x, непредсказуем.
При связывании указателя shared_ptr с простым указателем ответственность за эту память передается указателю shared_ptr. Как только ответственность за область памяти встроенного указателя передается указателю shared_ptr, больше нельзя использовать встроенный указатель для доступа к памяти, на которую теперь указывает указатель shared_ptr.
Другие операции с указателем shared_ptr
Класс shared_ptr предоставляет также несколько других операций, перечисленных в табл. 12.2 и табл. 12.3. Чтобы присвоить новый указатель указателю shared_ptr, можно использовать функцию reset():
p = new int(1024); // нельзя присвоить обычный указатель
// указателю shared_ptr
p.reset(new int(1024)); // ok: p указывает на новый объект
Подобно оператору присвоения, функция reset() модифицирует счетчики ссылок, а если нужно, удаляет объект, на который указывает указатель p. Функцию-член reset() зачастую используют вместе с функцией unique() для контроля совместного использования объекта несколькими указателями shared_ptr. Прежде чем изменять базовый объект, проверяем, является ли владелец единственным. В противном случае перед изменением создается новая копия:
if (!p.unique())
p.reset(new string(*p)); // владелец не один; резервируем новую копию
*p += newVal; // теперь, когда известно, что указатель единственный,
// можно изменить объект
Упражнения раздела 12.1.3
Упражнение 12.10. Укажите, правилен ли следующий вызов функции process(), определенной в текущем разделе. В противном случае укажите, как его исправить?
shared_ptr<int> p(new int(42));
process(shared_ptr<int>(p));
Упражнение 12.11. Что будет, если вызвать функцию process() следующим образом?
process(shared_ptr<int>(p.get()));
Упражнение 12.12. Используя объявления указателей p и sp, объясните каждый из следующих вызовов функции process(). Если вызов корректен, объясните, что он делает. Если вызов некорректен, объясните почему:
auto p = new int();
auto sp = make_shared<int>();
(a) process(sp);
(b) process(new int());
(c) process(p);
(d) process(shared_ptr<int>(p));
Упражнение 12.13. Что будет при выполнении следующего кода?
auto sp = make_shared<int>();
auto p = sp.get();
delete p;