Грокаем C++
5.09K subscribers
6 photos
3 files
278 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам - @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
std::unique_ptr

Снова пост по запросам подписчиков. @load_balancer попросил пояснить за std::unique_ptr и в чем его преимущества по сравнению со связкой new/delete. Когда-то я сам не понимал, в чем прикол этих умных указателей. Но со временем осознал, насколько это маст-хэв в современной плюсовой разработке.

Как было раньше. Концепция умных указателей появилась довольно давно и даже до стандартных классов, все пользовались или кастомными вариантами, или бустовскими. Но без семантики перемещения все работало довольно костыльно(яркий пример std::auto_ptr). Потому не буду брать это в расчет. Представим, что давным-давно не было никаких умных указателей. В С++ есть 2(4 если учитывать варианты для массивов) оператора для работы с кучей. new и delete. Когда нам нужен объект, время жизни которого мы хотим полностью контролировать от и до, мы вызываем оператор new. В этот момент происходят 2 вещи: выделяется память на куче и на этой памяти создается объект с помощью конструктора. И нам возвращается типизированный указатель на объект. Далее мы этим объектом работаем, работаем. И когда он нам больше не нужен, мы его удаляем с помощью delete. Тут тоже происходят 2 вещи: вызывается деструктор объекта и память возвращается системе. Когда-то все так писали и ничего страшного. В языке С до сих пор оперируют сырыми указателями и память менеджится самостоятельно.

Но у такого подхода есть проблемы. Системы, которые мы строим, имеют намного большую сложность, чем средний программист может воспринять. Иногда мы вообще работаем только с одной частью этой системы и чхать хотели на остальные компоненты. Все еще сильно усложняется тем, что приложения обычно многопоточные и мыслить в парадигме многопоточности - дело нетривиальное даже для самых профиков. Всвязи с этим логичное следствие - мы не до конца понимает и не осознаем полный цикл жизни объектов. А из этого проистекают 2 проблемы: человек может просто не удалить объект и удалить этот объект повторно. Первая приводит к утечкам памяти и все увеличивающему потреблению памяти приложением, а вторая приводит просто к падению этого приложения. Обе вещи пренеприятнейшие и хотелось бы не сталкиваться с ними.

Что придумали взрослые толстые дяди. В начале появилась идиома RAII. У нас есть довольно подробный пост на эту тему тут. Благодаря пониманию, что мы можем перенести ответственность за удаление объекта на систему при вызове деструктора, и благодаря внедрению move-семантики, появились привычные нам умные указатели std::unique_ptr и std::shared_ptr.

Их общее преимущество, по сравнению со старым подходом new/delete - разработчик теперь не заботится об удалении объекта. Он определяет только момент создания объекта и знает условия, при которых тот или иной умный указатель освободит память.

Специфика конкретно unique_ptr - объект этого указателя реализует семантику владения ресурсом. То есть нет другого объекта, который может повлиять на время жизни объекта-хозяина. Объект-хозяин может только удалиться сам и освободить ресурс, и передать права на владение ресурсом другому объекту. Все! Удаление происходит в предсказуемом моменте времени - при выходе из скоупа. А передача прав происходит при создании другого объекта в его конструкторе перемещения с явным вызовом std::move, который делает программист руками, когда хочет передать владение.

Нет ни одной причины не использовать unique_ptr для управления объектами. Памяти он обычно занимает столько же, как и обычный указатель. Благодаря нему мы пишем код в объектно-ориентированном стиле. И не заботимся о менеджменте памяти. Недостатков не наблюдаю.

Надеюсь, я довольно подробно описал мотивацию использования smart pointers и их преимущества. Будут вопросы - в удовольствием поговорим в комментах.

Stay smart. Stay cool.

#cpp11 #memory #goodpractice
Можно ли явно вызывать деструктор объекта?

Вопрос скорее из категории "А что если?" и скорее всего большинство разработчиков не столкнуться с надобностью это делать. Однако менее интересным этот вопрос не становится, если немного глубже копнуть. А копаться в плюсах всегда интересно)

В целом, любой публичный метод может быть вызван снаружи. Поэтому ответ на вопрос - да, можно. Но вот какие у этого будут последствия?

Возьмем простую структурку

struct SomeDefaultDestructedType
{
SomeDefaultDestructedType(int b): a{b} {}
~SomeDefaultDestructedType() = default;
int getNumber() {return a;}
int a;
};

Как видим, у нее дефолтовый деструктор, то есть отдает генерацию кода для него в руки компилятора. А компилятор в этих вопросах тупой и прямолинейный. Для нетривиальных полей он вызывает деструкторы, а для тривиальных типов - ничего не делает. И все. Поэтому для нашего класса вызывали мы деструктор для объекта, не вызывали, значения не имеет. Даже можно вызвать метод getNumber после явного вызова деструктора и он вернет валидное значение. Семантически объект уничтожен, а на самом деле живее всех живых. Память для него на стеке есть, данные в этой области лежат, поэтому мы и можем оперировать им как полноценным объектом. И хорошо, если у класса все его поля будут рекурсивно default deconstructed, тогда мы ничего не сломает. Проблемы начинаются, когда класс имеет нетривиальный деструктор. Посмотрим на следующую структурку:

struct SomeNonTrivialDestructedType
{
SomeNonTrivialDestructedType(int b): a{new int(b)} {}
~SomeNonTrivialDestructedType() { delete a;}
int getNumber() {return *a;}
int * a;
};

Простенькое выделение и освобождение памяти в куче. Что будет сейчас при явном вызове деструктора? Ресурс освободится, а объект останется лежать на стеке. А что происходит с объектами, которые выделяются в автоматической области, при выходе из области их создания? У них вызывается деструктор. Поэтому будет двойное освобождение памяти. А если этот объект еще использовать как-то, например вызвать у него метод getNumber, это еще и неопределенное поведение, так как обращаемся к освобожденной памяти.

Для объектов, созданных в куче через new, проблема остается такой же. Потому что хорошей практикой разработки является вызывать delete на каждый выделенный с помощью new объект. А delete вызывает деструктор.

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

Есть случаи, когда явный вызов деструктора оправдан, но это всегда связано с работой с памятью на низком уровне. Об этом поговорим как-нибудь в другой раз. А пока, если вам всерьез не приходила в голову мысль явно вызывать деструктор - и не нужно этого делать)

Stay safe. Stay cool.

#cppcore #fun #memory