В чем разница между std::unique_ptr и std::shared_ptr в C++?

Ответ

std::unique_ptr реализует исключительное владение (exclusive ownership). Ресурсом владеет ровно один unique_ptr в каждый момент времени. Копирование запрещено, перемещение — разрешено. Это легковесный указатель с нулевыми накладными расходами (по размеру и производительности сравним с сырым указателем).

std::shared_ptr реализует разделяемое владение (shared ownership) через подсчет ссылок. Ресурс уничтожается, когда удаляется последний shared_ptr, ссылающийся на него. Поддерживает копирование.

Сравнение:

Критерий std::unique_ptr<T> std::shared_ptr<T>
Владение Эксклюзивное Разделяемое
Накладные расходы Минимальные (указатель) Указатель + контрольный блок со счетчиком ссылок
Копирование Запрещено Разрешено (инкрементирует счетчик)
Перемещение Разрешено (передача владения) Разрешено

Примеры:

// unique_ptr: фабричная функция возвращает объект, которым должен владеть только вызывающий.
std::unique_ptr<MyClass> createResource() {
    return std::make_unique<MyClass>();
}
auto resource = createResource(); // Владение передано в resource
// auto resource2 = resource; // Ошибка компиляции: нельзя копировать
auto resource2 = std::move(resource); // OK: владение передано resource2, resource теперь nullptr

// shared_ptr: несколько компонентов системы используют один кэшированный ресурс.
auto cachedConfig = std::make_shared<Config>();
auto processor1 = cachedConfig; // Оба shared_ptr владеют одним объектом Config
auto processor2 = cachedConfig; // Счетчик ссылок = 3
processor1.reset(); // Счетчик ссылок = 2
// Объект Config будет уничтожен, когда выйдут из области видимости cachedConfig и processor2.

Важные замечания:

  • Циклические ссылки: shared_ptr могут создавать циклические зависимости, приводящие к утечкам памяти. Для их разрыва используется std::weak_ptr.
  • Правило: Всегда предпочитайте unique_ptr как более простой и эффективный инструмент. Используйте shared_ptr только тогда, когда семантика разделяемого владения действительно необходима.

Ответ 18+ 🔞

Давай разберем эту тему, как будто объясняю за рюмкой чая, а не на лекции по квантовой механике. Сиди, слушай.

Представь, что у тебя есть машина. std::unique_ptr — это как будто ты один-единственный владелец. Ключи только у тебя, доверия ебать ноль. Ты можешь отдать ключи другу (переместить), но тогда у тебя их уже не будет. А вот скопировать ключи и дать копию кому-то ещё — хуй тебе, а не копия. Компилятор тебе этого не позволит. Это легковесная штука, накладных расходов — как с сырым указателем, только безопаснее, потому что когда ты выходишь из тачки (указатель разрушается), она сама, блядь, на свалку едет (память освобождает). Удобно.

А вот std::shared_ptr — это уже общага. Тачка одна, а рулят ей все, кому не лень. У каждого по ключу (копия указателя). Пока хоть один ключ на руках — тачка никуда не денется. Как только последний чувак выкинул свой ключ в урну — всё, тачке пиздец, её утилизируют. За эту идиллию приходится платить: за каждым таким указателем тянется хвост — «контрольный блок», где сидит счетчик, кто сколько ключей на руках имеет. Тяжеловато, но иногда без этого никак.

Короче, табличка для тех, кто любит структуру:

Что сравниваем std::unique_ptr<T> std::shared_ptr<T>
Кто хозяин? Один царь и бог. Эксклюзив. Коммуналка. Всё общее.
Сколько весит? Почти ничего. Как голый указатель. Указатель + целый блок управления с счетчиком. Овердохуища метаданных.
Можно скопировать? Ни в коем случае. Копирование запрещено нахуй. Да запросто. Счетчик ссылок подкрутится и всё.
Можно передать? Да, но только перемещением. Старый владелец после этого — пустое место. Да, хоть копированием, хоть перемещением.

Примеры из жизни, то есть кода:

// unique_ptr: Ты сходил в магазин, купил хлеб. Он теперь только твой.
std::unique_ptr<MyClass> createResource() {
    return std::make_unique<MyClass>(); // Испекли булку
}
auto myBread = createResource(); // Владение передано тебе. Ты ешь.
// auto friendBread = myBread; // Ошибка! Хуй тебе, а не половина буханки. Не делится.
auto friendBread = std::move(myBread); // Ок, отдал всю булку другу. У тебя теперь пусто (nullptr).

// shared_ptr: Вы с соседом по общаге купили одну бутылку колы на двоих.
auto sharedCola = std::make_shared<Config>(); // Купили.
auto neighbor1 = sharedCola; // Сосед взял свою кружку.
auto neighbor2 = sharedCola; // Его девушка тоже хочет. Счетчик ссылок = 3.
neighbor1.reset(); // Первый сосед ушел на пары. Счетчик = 2.
// Бутылка будет стоять, пока sharedCola и neighbor2 не решат, что пить больше не хотят.

Важные нюансы, про которые забывают, а потом охуевают:

  • Циклические ссылки — это пиздец. Представь, что shared_ptr А указывает на Б, а shared_ptr Б указывает на А. Они друг друга держат, как два пьяных друга, и никто никогда не упадет. Утечка памяти гарантирована. Для таких хитрожопых случаев есть std::weak_ptr — он как слабенькая ссылочка, которая не мешает объекту умереть, когда пора.
  • Главное правило, ёпта: Если нет явной, острой необходимости делить один объект между двадцатью потоками — бери unique_ptr. Он проще, быстрее и не выест тебе мозг циклами. shared_ptr — это тяжёлая артиллерия, а не молоток для всего подряд. Используй его только когда семантика разделяемого владения реально нужна, а не «на всякий случай, авось пригодится».