Умный указатель unique_ptr

      Комментарии к записи Умный указатель unique_ptr отключены

В этой теме 1 ответ, 2 участника, последнее обновление  Васильев Владимир Сергеевич 1 год, 2 мес. назад.

  • Автор
    Сообщения
  • #2927

    questioner
    Участник

    Класс unique_ptr является наиболее простым объектом управления ресурсами (реализует идиому RAII) из стандартной библиотеки С++. В большинстве случаев он может заменить обычные указатели С++ и сделать код более безопасным, в том числе с точки зрения исключений. Расскажите о нем более подробно.

  • #2928

    Следующий пример (подобная ситуация часто возникает особенно у начинающих программистов) показывает проблемы с памятью, которые возникают при использовании обычных указателей:

    void functionA() {
      SomeType *ptrA = new SomeType;
      SomeType *ptrB = nullptr;
      
      functionB(); // some code
    
      if (functionC() == false) {
        delete ptrA;
        ptrA = nullptr;
      }
      else {
        ptrB = new SomeType();
      }
    
      functionD(); // some code
    
      if (ptrA)
        delete ptrA;
      if (ptrB)
        delete ptrB;
    }

    В этом коде не возникнет утечек если используемые в нем функции не будут возбуждать исключения. Однако, даже в этом случае код не самый безопасный, т.к. требует постоянно держать в голове указатели, по которым была выделена память. Очень легко забыть обнулить указатель после освобождения памяти — при этом память будет ошибочно освобождена по одному указателю дважды.

    Если бы мы попробовали сделать этот маленький фрагмент кода безопасным с точки зрения исключений — возникло бы множество проблем и объем кода вырос бы в разы. Если исключение возникнет в functionB — нужно освободить память из под ptrA, в functionD — из под ptrB, однако оно может возникнуть и в functionС.

    Приведенный код можно переписать более правильно и безопасно разделив на более мелкие функции, однако этот пример призван показать проблемы, возникающие в более сложных случаях. Иногда их можно решить при помощи ссылок, однако не всегда (см. Указатели и ссылки в С++). Такая проблема будет возникать например если в класс вложено два или более объектов с разным уровнем жизни — мы не можем использовать ссылки и вынуждены выполнять проверки указателей в деструкторе:
    if (ptr) delete ptr;

    Можно выделить следующие признаки проблемного кода, который можно улучшить с использованием умных указателей:

    1. память под объекты выделяется или освобождается внутри условных блоков;
    2. в середине функции (когда была выделена память под часть объектов) функция может быть завершена (вызван return или возбуждено исключение);
    3. внутри одной области видимости существует несколько объектов с различным временем жизни.

    В стандартной библиотеке языка С++ определено несколько видов умных указателей, которые являются более удобной и безопасной альтернативой сырых указателей (T*). В большинстве случаев для замены сырых указателей достаточно использовать именно std::unique_ptr. Этот класс реализует идиому RAII — является объектом управления памятью [1], т.е.:

    1. владеет объектом через его указатель;
    2. разрушает объект когда unique_ptr выходит из области видимости или получает новое значение;
    3. ведет себя в основном как обычный указатель — его можно разыменовать (получить объект) оператором * или обратиться к членам через оператор ->.

    С использованием unique_ptr приведенный выше пример можно переписать следующим образом:

    void functionA() {
      unique_ptr<SomeType> ptrA(new SomeType);
      unique_ptr<SomeType> ptrB(nullptr);
      
      functionB(); // some code
    
      if (functionC() == false) {
         ptrA.release();
      }
      else {
        ptrB = unique_ptr<SomeType>();
      }
    
      functionD(); // some code
    }

    Видно, что в конце функции нет вызовов delete, теперь мы не освобождаем память вручную — этим занимается умный указатель. Сам умный указатель объявлен на стеке, т.к. объекты ptrA и ptrB будут вызваны при выходе из функции любым способом (return или необработанном исключении), т.к. такой выход сопровождается расруткой стека. Уничтожение умного указателя приведет к освобождению памяти хранимого внутри него объекта, т.е. утечек не будет в любом случае. Код стал проще и гораздо безопаснее.

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

    Указатель unique_ptr невозможно скопировать, т.к. это нарушало бы его логику — объект, хранимый в указателе должен быть только один и только в одной области видимости. Для реализации такого поведения конструктор копирования этого класса удален (=delete) или находится в защищенной секции (см. «Приватный конструктор в С++«. Кроме того, удаленным для этого класса является копирующий оператор присваивания — принимающий lvalue-ссылку (см. Ссылки в С++: rvalue- и lvalue- references [5]).

    Вместо копирующих операций предлагается использовать перемещающие — класс unique_ptr поддерживает семантику перемещения, введенную в С++11 (см. Перемещающий конструктор и семантика перемещения). Это значит, что для класса определена операция присваивания для случаев, когда в правой части операции стоит временный объект и такой же конструктор (перемещающий). Эти операции вместо копирования объекта, хранимого внутри unique_ptr выполняют его перемещение из одной области видимости в другую. Это позволяет использовать unique_ptr в качестве возвращаемых значений функций и их аргументов (причем это также почти также эффективно, как использование сырых указателей).

    #include <memory>
    #include <iostream>
    using namespace std;
    
    struct SomeType {
      SomeType() {
        cout << "constructor:" << this << endl;
      }
      ~SomeType() {
        cout << "destructor:" << this << endl;
      }
    };
    
    void sink(unique_ptr<SomeType> ptr) {
      cout << "into sink()" << endl;
    }
    
    unique_ptr<SomeType> source() {
      cout << "into source()" << endl;
      return unique_ptr<SomeType>(new SomeType());
    }
    
    int main() {
      cout << "into main():" << endl;
      unique_ptr<SomeType> ptr = source();
      cout << "into main():" << endl;
      //sink(ptr); // ERROR: unique_ptr(const unique_ptr&) = delete;
      sink(move(ptr));
      cout << "into main()" << endl;
    }

    Приведенный код демонстрирует семантику перемещения, запустив его, вы увидите, что конструктор вызывается в функции source(), а деструктор — в функции sink(). Объект класса SomeType создается внутри функции source(), при ее завершении умный указатель будет уничтожен. Однако эта функция возвращает его в качестве результата, поэтому должен быть вызван перемещающий конструктор и создан временный объект, содержимого которого потом будет перемещено в функцию main(), однако в этом случае многие компиляторы проводят оптимизацию возвращаемого значения (RVO/NRVO). Объект попадает в функцию main() откуда перемещается внутрь функции sink(), в main() при этом остается пустой указатель. В функции sink() объект SomeType уничтожается, а в функции main() перед завершением работы разрушится unique_ptr, с нулевым указателем. Весь этот процесс показан на диаграмме:

    move semantic unique_ptr

    В нашем коде закомментирована строка, содержащая вызов конструктора копирования, т.к. она приведет к ошибке времени компиляции. Однако, семантика перемещения позволяет писать, например такой код:
    sink(source());
    Тут в функцию sink будет передан временный объект, поэтому сработает уже не конструктор копирования, а перемещающий конструктор.

    Благодаря поддержки семантики перемещения объекты unique_ptr очень эффективны при использовании в качестве элементов контейнеров (vector<unique_ptr<SomeType> >). При сортировке элементов контейнера фактически будут переставляться сырые указатели на объекты. Даже если сам объект не поддерживает семантику перемещения (унаследованный код) или она не может быть для него эффективно реализована (простейший пример — класс точки пространства, см. Перемещающий конструктор и семантика перемещения), unique_ptr сделает код почти таким же эффективным, как при использовании сырых указателей, но при этом безопасным.

    Наконец, нужно понимать, что умные указатели — не панацея, у вас в любом случае остается возможность написать опасный код — это будет случаться всякий раз, когда в вашей программе появятся сырые указатели. Следующий код демонстрирует типичную ошибку (часто возникает при поддержке унаследованного кода и использовании умных указателей):

    // WRONG CODE !
    void foo() {
      SomeType *raw_ptr = new SomeType;
      unique_ptr<SomeType> ptr(raw_ptr);
      
      delete raw_ptr;
    }

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

    В связи с этим, часто возникают вопросы по поводу функции get(), которую предоставляет unique_ptr — эта функция возвращает сырой указатель. Эта функция чаще всего используется когда в некоторой области видимости, вложенной в область видимости unique_ptr часто используется сырой указатель (например, этого требует унаследованный код):

    {
      // Scope unique_ptr
      auto ptr = make_unique<SomeType>();
      {
        // Scope raw_ptr
        SomeType *raw_ptr = ptr.get();
        // use raw_ptr, but not delete it
      }
    }

    При таком использовании проблем с функцией get() не возникнет, однако, если в будущем кто-нибудь «найдет утечку» и добавит вызов delete — программа начнет аварийно падать. Область видимости сырого указателя должна быть при этом всегда вложена в область видимости умного, иначе вы будете периодически пытаться использовать невалидный сырой указатель:

    {
      // Scope raw_ptr 
      SomeType *raw_ptr; // (data of class cor example) 
      {
        // Scope unique_ptr (function of class for example) 
        auto ptr = make_unique<SomeType>(); 
        raw_ptr = ptr.get();
      } // ptr destructed
      raw_ptr->foo(); // ERROR: raw_ptr is invalid 
    }

    Таким образом:

    1. объекты unique_ptr хранят внутри сырой указатель и поддерживают семантику перемещения, за счет этого они почти настолько же эффективны, как и сырые указатели, по при этом безопасны, т.к. деструктор объекта, расположенного по указателю будет вызван при выталкивании из стека объекта умного указателя (исключении или выходе из области видимости);
    2. в большинстве случаев unique_ptr может заменить сырые указатели;
    3. с помощью unique_ptr можно добиться эффективной и безопасной обработки массива объектов, не поддерживающих семантику перемещения;
    4. само по себе использование unique_ptr — не панацея, чтобы минимизировать число ошибок нужно или вовсе отказаться от сырых указателей и функции unique_ptr::get(), или использовать их очень аккуратно.

    Вспомогательная литература:

    1. Идиома RAII. Объекты управления ресурсами в C++ — объект unique_ptr размещается на стеке и владеет другим объектом через указатель, автоматически вызывая деструктор (например если код возбудит исключение). В более общем случае такое поведение называется идиомой RAII;
    2. Перемещающий конструктор и семантика перемещения — описывается семантика перемещения, реализованная в std::unique_ptr;
    3. Указатели и ссылки в С++ — рассказывается про то, почему ссылки более безопасны, чем указатели, а также приведены случаи, когда использованием ссылок обойтись не получится (ответ на вопрос «зачем нужны указатели?»);
    4. Приватный конструктор в С++ — описывается как запретить копирование объектов класса;
    5. Ссылки в С++: rvalue- и lvalue- references — описываются lvalue- и rvalue- ссылки в С++, показывается как с помощью rvalue-ссылок может быть написан более оптимальный код.

Для ответа в этой теме необходимо авторизоваться.