Что происходит, когда необработанный указатель из shared_ptr get () удаляется? - PullRequest
0 голосов
/ 24 декабря 2018

Я написал некоторый код, подобный следующему:

shared_ptr<int> r = make_shared<int>();
int *ar = r.get();

delete ar; // report double free or corruption
// still some code

Когда код достиг значения delete ar;, программа завершилась сбоем и сообщила «двойное освобождение или повреждение», я запутался, почему двойное освобождение?Буква «r» все еще находится в области видимости и не извлекается из стека.Оператор удаления делает что-то волшебное ??Знает ли он, что необработанный указатель в настоящее время обрабатывается умным указателем?а затем счетчик в "r" будет автоматически уменьшен до нуля?Я знаю, что операции не рекомендуется, но я хочу знать, почему?

Ответы [ 4 ]

0 голосов
/ 05 января 2019

Конечно, это зависит от того, как библиотека реализует make_shared, однако наиболее вероятная реализация такова:

std :: make_shared выделяет один блок для двух вещей:

  • блок управления указателем общего доступа
  • содержащийся объект

std :: make_shared () вызовет распределитель памяти один раз, а затем дважды вызовет размещение нового для инициализации (конструкторов вызовов) упомянутых двух вещей.

|           block requested from allocator          |
| shared_ptr control block |         X object       |
#1                         #2                       #3

Это означает, что распределитель памяти предоставил один большой блок, адрес которого # 1.Общий указатель затем использует его для управляющего блока (# 1) и фактического содержимого объекта (# 2).Когда вы вызываете delete с фактическим объектом, хранящимся в shred_ptr (.get ()), вы вызываете delete (# 2).Поскольку # 2 не известен распределителю, вы получаете ошибку повреждения.

0 голосов
/ 24 декабря 2018

Вы удаляете указатель, который не пришел из new, поэтому у вас неопределенное поведение (может произойти все что угодно).

Из cppreference на delete :

Для первой формы (не массива) выражение должно быть указателем на тип объекта или тип класса, контекстуально неявно преобразуемый в такой указатель, и его значение должно быть либо нулевым, либо указателем на объект, не являющийся массивомсозданный новым выражением или указателем на базовый подобъект объекта, не являющегося массивом, созданного новым выражением.Если выражение - это что-то еще, в том числе указатель, полученный в виде массива new-expression, поведение не определено.

Если выделение выполняется с помощью new, мы можем быть уверены,что у нас есть указатель, который мы можем использовать delete.Но в случае shared_ptr.get() мы не можем быть уверены, что сможем использовать delete, поскольку это может быть не фактический указатель, возвращаемый new.

0 голосов
/ 24 декабря 2018
shared_ptr<int> r = make_shared<int>();

Нет никакой гарантии, что это вызовет new int (что в любом случае не является строго наблюдаемым пользователем) или, в более общем смысле, new T (что наблюдается для определенного пользователем, специфичного для класса operator new);на практике это не произойдет (нет гарантии, что это не произойдет).

Последующее обсуждение касается не только shared_ptr, но и «умных указателей» с владением семантика.Для любого интеллектуального указателя smart_owning :

Основная мотивация make_owning вместо smart_owning<T>(new T) состоит в том, чтобы в любой момент избежать выделения памяти без владельца;это было важно в C ++, когда порядок вычисления выражений не давал гарантии, что оценка подвыражений в списке аргументов была непосредственно перед вызовом этой функции;исторически в C ++:

f (smart_owning<T>(new T), smart_owning<U>(new U));

можно оценить как:

T *temp1 = new T;
U *temp2 = new U;
auto &&temp3 = smart_owning<T>(temp1);
auto &&temp4 = smart_owning<U>(temp2);

Таким образом, temp1 и temp2 не управляются никаким объектом-владельцем в течение нетривиального времени:

  • очевидно, new U может выдать исключение
  • создание интеллектуального указателя-владельца обычно требует выделения (небольших) ресурсов и может выдать

Так что либоtemp1 или temp2 могут быть пропущены (но не оба), если возникнет исключение, что и было именно той проблемой, которую мы пытались избежать в первую очередь.Это означает, что составные выражения, включающие конструирование умных указателей, были плохой идеей;это нормально:

auto &&temp_t = smart_owning<T>(new T);
auto &&temp_u = smart_owning<U>(new U);
f (temp_t, temp_u);

Обычно выражения, включающие столько подвыражений с вызовами функций, сколько f (smart_owning<T>(new T), smart_owning<U>(new U)) считаются разумными (это довольно простое выражение с точки зрения количества подвыражений).Запрещение таких выражений довольно раздражает и очень трудно оправдать.

[Это одна из причин, и, на мой взгляд, самая убедительная причина, почему недетерминизм порядка оценки был удален комитетом по стандартизации C ++ такчто такой код не является безопасным.(Это было проблемой не только для выделенной памяти, но и для любого управляемого выделения, такого как дескрипторы файлов, дескрипторы базы данных ...)]

Поскольку код часто нуждался в таких вещах, как smart_owning<T>(allocate_T()) в подвыраженияхи поскольку указание программистам разлагать умеренно сложные выражения, включающие в себя распределение во многих простых строках, не было привлекательным (больше строк кода не означает, что их легче читать), авторы библиотек предоставили простое исправление: функцию, позволяющую создатьобъект с динамическим временем жизни и создание собственного объекта вместе.Это решило проблему порядка оценки (но поначалу было сложно, потому что требовалась совершенная пересылка аргументов конструктора).

Предоставление двух задач функции (выделение экземпляра T и экземпляраsmart_owning) дает свободу проводить интересную оптимизацию: вы можете избежать одного динамического распределения, поместив управляемый объект и его владельца рядом друг с другом.

Но, опять же, это было не основное назначение таких функций, как make_shared.

Поскольку интеллектуальные указатели с эксклюзивным владением по определению не должны вести подсчет ссылок и по определению не должны делитьсяданные, необходимые для удаления, либо между экземплярами, и, следовательно, могут хранить эти данные в «умном указателе» (*), для построения unique_ptr дополнительное выделение не требуется;Тем не менее, был добавлен шаблон функции make_unique, чтобы избежать проблемы с висящим указателем, а не оптимизировать небытие (выделение, которое не выполняется в первую очередь).

(*), что означает BTWуникальный владелец "умные указатели" делают не имеют указатель семантический, поскольку семантический указатель подразумевает, что вы можете делать копии "указателя", и вы не можете иметь две копии уникальноговладелец указывает на тот же экземпляр;«умные указатели» никогда не были указателями в любом случае, термин вводит в заблуждение.

Резюме:

make_shared<T> выполняет необязательную оптимизацию , где нет отдельного динамического выделения памяти для T: нет operator new(sizeof (T)).Очевидно, что все еще создается экземпляр с динамическим временем жизни с другим operator new: размещением new .

Если вы замените явное освобождение памяти явным уничтожением и добавите паузу сразу после этой точки:

class C { 
public:
  ~C();
};
shared_ptr<C> r = make_shared<C>();
C *ar = r.get();
ar->~C();
pause(); // stops the program forever

Программа, вероятно, будет работать нормально; все еще нелогично, недопустимо, некорректно явно уничтожать объект, управляемый умным указателем .Это не "твой" ресурс.Если pause() может выйти с исключением, умный указатель-владелец попытается уничтожить управляемый объект, который даже больше не существует.

0 голосов
/ 24 декабря 2018

См. здесь .Я цитирую:

std :: shared_ptr - это интеллектуальный указатель, который сохраняет общее владение объектом через указатель.Несколько объектов shared_ptr могут иметь один и тот же объект.Объект уничтожается, и его память освобождается, когда происходит одно из следующих событий:

  • последний оставшийся shared_ptr, владеющий объектом, уничтожается;
  • последний оставшийся shared_ptr, владеющий объектом, назначается другомууказатель с помощью оператора = или reset ().

Объект уничтожается с использованием выражения delete или пользовательского удалителя, который передается в shared_ptr во время построения.

Итак, указательудаляется shared_ptr.Вы не должны удалять сохраненный указатель самостоятельно

ОБНОВЛЕНИЕ:

Я не осознавал, что было больше операторов, и указатель не вышелиз области видимости, извините.

Я читал больше, и стандарт мало что говорит о поведении get(), но здесь - примечание, я цитирую:

shared_ptr может совместно владеть объектом при сохранении указателя на другой объект.get () возвращает сохраненный указатель, а не управляемый указатель.

Таким образом, похоже, что разрешено, что указатель, возвращаемый get(), не обязательно совпадает с указателем, выделенным shared_ptr (предположительно используя new).Так что delete этот указатель является неопределенным поведением.Я буду более подробно разбираться в деталях.

ОБНОВЛЕНИЕ 2:

Стандарт гласит § 20.7.2.2.6 (о make_shared):

6 Примечания: Реализации рекомендуется, но не обязательно, выполнять не более одного выделения памяти.[Примечание: это обеспечивает эффективность, эквивалентную навязчивому интеллектуальному указателю.- примечание конца]

7 [Примечание: Эти функции обычно выделяют больше памяти, чем sizeof (T), чтобы учесть внутренние структуры бухгалтерского учета, такие как счетчик ссылок.- конец примечания]

Таким образом, конкретная реализация make_shared может выделить один кусок памяти (или больше) и использовать часть этой памяти для инициализации сохраненный указатель (но, возможно, не вся выделенная память).get() должен возвращать указатель на сохраненный объект, но стандарт не требует, как было сказано ранее, что указатель, возвращаемый get(), должен быть указателем, выделенным new.Так что delete этот указатель является неопределенным поведением, вы получили сигнал, но все может произойти.

...