Умные указатели в C++

Автор: Igor Semenov
www.progz.ru

Источник: RSDN Magazine #1-2008
Опубликовано: 17.07.2008
Версия текста: 1.0
std::auto_ptr<>
std::tr1::shared_ptr<>
std::tr1::weak_ptr<>
boost::intrusive_ptr<>
boost::scoped_ptr<>
boost::scoped_array<>, boost::shared_array<>
std::unique_ptr<>
Другие реализации
О чём следует помнить, используя умные указатели
Заключение
Источники мудрости
Благодарности

Работа с динамической памятью является одним из самых важных моментов в программировании на C++. И в то же время, эта область является источником огромного количества ошибок в программах, начиная от банальных утечек памяти и заканчивая нарушениями защиты.

В этой статье приводится обзор инструментов, позволяющих избежать части проблем, связанных с использованием динамической памяти в C++. Описанные ниже инструменты и методики не являются панацеей от проблем управления памятью, они всего лишь способны облегчить жизнь программисту при условии правильного их использования. Эти инструменты носят общее название «умные указатели» («smart pointers»), что подразумевает их семантическое сходство с обыкновенными указателями C++. Это сходство обеспечивается перегрузкой операторов *, ->, &, и в некоторых случаях оператора []. Все умные указатели реализованы в виде шаблонов, что позволяет использовать их для любых встроенных и пользовательских типов.

Прежде чем переходить к рассмотрению реализаций умных указателей, следует обратить внимание на проблемы, связанные с использованием обычных указателей:

  1. Указатель не управляет временем жизни объекта, ответственность за удаление объекта целиком лежит на программисте. Проще говоря, указатель не «владеет» объектом.
  2. Указатели, ссылающиеся на один и тот же объект, никак не связаны между собой. Это создаёт проблему «битых» указателей – указателей, ссылающихся на освобождённые или перемещённые объекты.
  3. Нет никакой возможности проверить, указывает ли указатель на корректные данные, либо «в никуда».
  4. Указатель на единичный объект и указатель на массив объектов никак не отличаются друг от друга.

Рассмотрим по порядку эти недостатки, а также инструменты, позволяющие от них избавиться.

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

      void SomeMethod()
{
  SomeClass* temp = new SomeClass;
  temp->DoSomething();
  delete temp;
}

Всё выглядит довольно надёжно: создаём объект, используем, затем уничтожаем. Но это только до тех пор, пока мы не вспомним об исключениях. Метод DoSomething вполне может сгенерировать исключение, в результате чего SomeMethod завершится, так и не освободив temp. Чтобы исключить утечку памяти, приходится писать примерно такой код:

      void SomeMethod()
{
  SomeClass* temp = 0;

  try
  {
    temp = new SomeClass;
    temp->DoSomething();
  }
  catch (...)
  {
    delete temp;
    throw;
  }

  delete temp;
}

Аналогичные нагромождения приходится возводить и в функциях с множественными точками выхода (т.е. с несколькими конструкциями return). А если объектов создаётся несколько, то код вообще разрастается до невероятных размеров.

Другой классический пример:

      class MyClass
{
public:
  MyClass() : a_(new SomeClass), b_(new SomeOtherClass) { }

  ~MyClass() throw()
  {
    delete a_;
    delete b_;
  }

private:
  SomeClass* a_;
  SomeOtherClass* b_;
};

В этом случае, если конструктор SomeOtherClass выбросит исключение, экземпляр SomeClass будет утерян навсегда. Дело в том, что по стандарту C++ [1], при раскрутке стека в результате возникновения исключения уничтожаются только полностью созданные объекты, а MyClass таким не является – ведь управление не доходит до тела конструктора. В результате будут вызваны только деструкторы членов класса, а в случае MyClass деструкторы эти ничего не делают, т.к. a_ и b_ являются обыкновенными указателями. Всё это приводит к утечке памяти.

Очевидное решение: вместо обычного указателя использовать объект, хранящий указатель и освобождающий объект в своём деструкторе. Такая технология называется RAII, Resource Acquisition Is Initialization – «Захват ресурса есть инициализация». RAII – это популярный паттерн проектирования, применяемый во многих объектно-ориентированных языках (например, C++, D, Ada), смысл которого заключается в том, что захват ресурса совмещается с инициализацией объекта, а освобождение – с финализацией (уничтожением) объекта. Более подробно о RAII можно почитать в [2], [3] и [4]). Именно таким поведением обладает шаблон auto_ptr из стандартной библиотеки C++.

std::auto_ptr<>

auto_ptr является умным указателем, реализующим семантику владения. Стоит отметить, что auto_ptr – это единственный умный указатель, включенный в нынешний стандарт C++ [1].

Вернёмся к нашим примерам и перепишем их с использованием auto_ptr:

      void SomeMethod()
{
  std::auto_ptr<SomeClass> temp(new SomeClass);
  temp->DoSomething();
}

class MyClass
{
public:
  MyClass() : a_(new SomeClass), b_(new SomeOtherClass) {}

  ~MyClass() throw() {}

private:
  std::auto_ptr<SomeClass> a_;
  std::auto_ptr<SomeOtherClass> b_;
};

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

Посмотрим на auto_ptr внимательнее. Этот шаблон определён в стандартном заголовке memory:

      namespace std 
{
  template<class Y> struct auto_ptr_ref {};

  template<class X> class auto_ptr 
  {
  public:
    typedef X element_type;

    explicit auto_ptr(X* p = 0) throw();
    auto_ptr(auto_ptr&) throw();
    template<class Y> auto_ptr(auto_ptr<Y>&) throw();
    auto_ptr& operator=(auto_ptr&) throw();
    template<class Y> auto_ptr& operator=(auto_ptr<Y>&) throw();
    auto_ptr& operator=(auto_ptr_ref<X> r) throw();

    ~auto_ptr() throw();

    X& operator*() constthrow();
    X* operator->() constthrow();
    X* get() constthrow();
    X* release() throw();
    void reset(X* p = 0) throw();

    auto_ptr(auto_ptr_ref<X>) throw();
    template<class Y> operator auto_ptr_ref<Y>() throw();
    template<class Y> operator auto_ptr<Y>() throw();
  };
}

Отдельно следует обратить внимание на копирующие конструкторы и операторы присваивания. Здесь проявляется одна любопытная особенность auto_ptr, незнание которой чревато серьёзными ошибками. Дело в том, что auto_ptr реализует так называемое разрушающее копирование. Рассмотрим эту особенность на примере:

std::auto_ptr<A> a(new A);
std::auto_ptr<A> b(a);

Сначала мы создаём экземпляр класса A и передаём его во владение умному указателю a. Затем мы создаём ещё один умный указатель b и передаём владение экземпляром A ему. При этом указатель a должен быть сброшен, иначе одним и тем же экземпляром объекта будут владеть два умных указателя, и в результате мы получим двойное удаление одного и того же экземпляра при выходе из области видимости. Аналогичное поведение мы увидим и при присваивании:

b = a;

Разрушающее копирование – это особенность поведения auto_ptr, заложенная в его архитектуре.

Кроме того, существует пустой шаблон auto_ptr_ref. Он является уловкой реализации, благодаря которой нельзя использовать в операции присваивания константные auto_ptr. Подробнее об этом можно почитать в [5].

Подведём итоги: шаблон auto_ptr обеспечивает владение экземпляром объекта и его корректную очистку при выходе из области видимости – иными словами, реализует принцип RAII. Тем не менее, следует принимать во внимание необычную семантику копирования и присваивания – разрушающее копирование. В частности, эта семантика не позволяет использовать auto_ptr в контейнерах стандартной библиотеки.

Выходом из этой ситуации является реализация умного указателя с подсчётом ссылок – shared_ptr.

std::tr1::shared_ptr<>

shared_ptr является более гибкой и универсальной версией умного указателя. shared_ptr не входит в текущую версию стандарта, однако этот класс уже включён в TR1 (Technical Report 1) [6], что делает его верным кандидатом на включение в следующую редакцию стандарта – C++0x. Рассмотрим объявление этого шаблона:

      namespace std 
{
  namespace tr1 
  {
    template<class T> class shared_ptr 
    {
    public:
      typedef T element_type;

      shared_ptr();
      template<class Y> explicit shared_ptr(Y* p);
      template<class Y, class D> shared_ptr(Y* p, D d);
      shared_ptr(shared_ptr const& r);
      template<class Y> shared_ptr(shared_ptr<Y> const& r);
      template<class Y> explicit shared_ptr(weak_ptr<Y> const& r);
      template<class Y> explicit shared_ptr(auto_ptr<Y>& r);

      ~shared_ptr();

      shared_ptr& operator=(shared_ptr const& r);
      template<class Y> shared_ptr& operator=(shared_ptr<Y> const& r);
      template<class Y> shared_ptr& operator=(auto_ptr<Y>& r);

      void swap(shared_ptr& r);
      void reset();
      template<class Y> void reset(Y* p);
      template<class Y, class D> void reset(Y* p, D d);

      T* get() const;
      T& operator*() const;
      T* operator->() const;
      long use_count() const;
      bool unique() const;
      operator unspecified-bool-type () const;
    };

    template<class T, class U> 
    booloperator==(shared_ptr<T> const& a, shared_ptr<U> const& b);
    template<class T, class U> 
    booloperator!=(shared_ptr<T> const& a, shared_ptr<U> const& b);
    template<class T, class U> 
    booloperator<(shared_ptr<T> const& a, shared_ptr<U> const& b);

    template<class E, class T, class Y>
    basic_ostream<E, T>& operator<<(basic_ostream<E, T>& os, 
                                    shared_ptr<Y> const& p);

    template<class T> void swap(shared_ptr<T>& a, shared_ptr<T>& b);

    template<class T, class U> 
    shared_ptr<T> static_pointer_cast(shared_ptr<U> const& r);
    template<class T, class U> 
    shared_ptr<T> dynamic_pointer_cast(shared_ptr<U> const& r);
    template<class T, class U> 
    shared_ptr<T> const_pointer_cast(shared_ptr<U> const& r);

    template<class D, class T> D* get_deleter(shared_ptr<T> const& p);
  } // namespace tr1
} // namespace std

shared_ptr обладает во многом схожей с auto_ptr семантикой, основные отличия заключаются в методике копирования. Класс shared_ptr реализует разделяемое (shared) владение с подсчётом ссылок, что позволяет использовать более привычные конструкторы копирования и операторы присваивания. Благодаря такой семантике shared_ptr можно использовать в стандартных контейнерах STL.

Кроме того, shared_ptr позволяет задавать функтор удаления (deleter), что может быть полезно для классов, имеющих необычную семантику удаления. В качестве небольшого приятного дополнения – оператор неявного преобразования в bool, что позволяет писать привычное:

std::tr1::shared_ptr<A> p = some_ptr;
if (p)
{
  // ...
}

В итоге мы имеем реализацию умного указателя, экземпляры которого совместно владеют экземпляром объекта, на который они удерживают ссылку. Совместное владение основано на механизме подсчёта ссылок. Такая семантика позволяет использовать shared_ptr вместо обычного указателя, практически полностью сохраняя поведение.

Представим себе случай, когда нам нужно передать указатель, не передавая права владения объектом. Использовать для этой цели shared_ptr нельзя, т.к. его копирование увеличит счётчик ссылок, и в результате происходит разделение прав владения. Использование обычного указателя также нежелательно, т.к., как уже говорилось в самом начале, невозможно проверить, ссылается указатель на существующий объект или нет.

Специально для такого случая в TR1 предусмотрен ещё один вид умных указателей: weak_ptr.

std::tr1::weak_ptr<>

weak_ptr – это «слабый», не владеющий экземпляром объекта умный указатель. Вот как выглядит его объявление:

      namespace std 
{
  namespace tr1 
  {
    template<class T> class weak_ptr
    {
    public:
      typedef T element_type;
      // constructors
      weak_ptr();
      template<class Y> weak_ptr(shared_ptr<Y> const& r);
      weak_ptr(weak_ptr const& r);
      template<class Y> weak_ptr(weak_ptr<Y> const& r);
      // destructor
      ~weak_ptr();
      // assignment
      weak_ptr& operator=(weak_ptr const& r);
      template<class Y> weak_ptr& operator=(weak_ptr<Y> const& r);
      template<class Y> weak_ptr& operator=(shared_ptr<Y> const& r);
      // modifiersvoid swap(weak_ptr& r);
      void reset();
      // observerslong use_count() const;
      bool expired() const;
      shared_ptr<T> lock() const;
    };

    // comparisontemplate<class T, class U> 
    booloperator<(weak_ptr<T> const& a, weak_ptr<U> const& b);
    // specialized algorithmstemplate<class T> void swap(weak_ptr<T>& a, weak_ptr<T>& b);

  } // namespace tr1
} // namespace std

weak_ptr ссылается на экземпляр, которым владеет shared_ptr, однако не разделяет прав владения этим экземпляром. Это гарантирует, что если экземпляр уже уничтожен, мы сможем это надёжно и безопасно проверить.

      class MyClass
{
public:
  MyClass(const std::tr1::weak_ptr<SomeClass>& ptr) :
      ptr_(ptr)
  {
  }

  void SomeMethod()
  {
    if (!ptr_.expired())
    {
      std::tr1::shared_ptr<SomeClass> realPtr(ptr_.lock());
      realPtr->DoSomething();
    }
  }

private:
  std::tr1::weak_ptr<SomeClass> ptr_;
};

Следует отметить, что хотя shared_ptr и weak_ptr пока ещё не входят в официальный стандарт C++, они уже поддерживаются многими современными компиляторами, например GCC 4.1.2 и Visual Studio 2008.

Ниже приводятся краткие описания ещё нескольких экзотических вариантов умных указателей из библиотеки boost.

boost::intrusive_ptr<>

intrusive_ptr представляет собой облегчённую версию shared_ptr, специально предназначенную для классов, имеющих встроенные механизмы подсчёта ссылок. Для таких классов intrusive_ptr позволяет реализовать эффективный механизм совместного владения с подсчётом ссылок, но без дополнительных затрат. Соответственно, отсутствует и аналог weak_ptr. Во всём остальном семантика intrusive_ptr повторяет семантику shared_ptr.

Для реализации счётчика ссылок intrusive_ptr требует наличия функций intrusive_ptr_add_ref и intrusive_ptr_release, принимающих в качестве параметра указатель на экземпляр класса. intrusive_ptr_add_ref вызывается для увеличения счётчика ссылок, intrusive_ptr_release – для его уменьшения. Есть небольшая хитрость, касающаяся пространства имён этих функций. Для компиляторов, поддерживающих ADL (Argument Dependent Lookup – правило, позволяющее искать свободные функции в пространствах имён их аргументов; более подробно об ADL можно почитать в [2]), можно определить эти функции в пространстве имён, соответствующем классу, экземпляром которого управляет intrusive_ptr. Если ваш компилятор не поддерживает ADL, придётся объявлять эти функции в пространстве имён boost.

      namespace boost
{
  void intrusive_ptr_add_ref(MyClass* p)
  {
    p->AddRef();
  }
  void intrusive_ptr_release(MyClass* p)
  {
    p->Release();
  }
}

…

boost::intrusive_ptr<MyClass> ptr(new MyClass);
ptr->DoSomething();

boost::scoped_ptr<>

scoped_ptr представляет собой облегчённую версию auto_ptr, запрещающую копирование и присваивание. По сути это всего-навсего аналог const std::auto_ptr<>.

boost::scoped_array<>, boost::shared_array<>

Классы scoped_array и shared_array представляют собой семантические аналоги scoped_ptr и shared_ptr, но применимо к динамически выделяемым массивам объектов. Отличие состоит только в реализации оператора [] вместо операторов * и ->. Кроме того, shared_array в отличие от shared_ptr не реализует копирующие конструкторы и операторы присваивания, позволяющие преобразовывать типы значений – такие преобразования опасны и в большинстве случаев приводят к неопределённому поведению, либо к нарушению защиты памяти:

      class A { };
class B : public A { };

std::tr1::shared_ptr<B> b(new B);
std::tr1::shared_ptr<A> a(b); // OK

boost::shared_array<B> b_arr(new B[ 10 ]);
boost::shared_array<A> a_arr(b_arr); // к счастью, не скомпилируется

В заключение хочется отметить, что в подавляющем большинстве случаев не рекомендуется использовать динамические массивы, предпочитая им стандартные контейнеры STL, например std::vector. Такой подход позволит избежать множества проблем, почти ничего не теряя.

std::unique_ptr<>

Свежий черновик нового стандарта C++0x объявляет auto_ptr устаревшим (obsolete), предлагая использовать вместо него unique_ptr. Его основное отличие от auto_ptr – это возможность использования функтора удаления (deleter), аналогично тому, как это реализовано в shared_ptr, что обеспечивает дополнительную гибкость. Например, unique_ptr позволяет хранить указатели на массивы, чего не позволяет auto_ptr. Таким образом, unique_ptr заменяет сразу auto_ptr и scoped_array. unique_ptr имеет другую семантику копирования и присваивания, использующую анонсированные в C++0x ссылки на временные объекты (R-value references). В качестве приятного дополнения – неявное преобразование к bool, что позволяет использовать unique_ptr в логических выражениях (опять же, по аналогии с shared_ptr).

Мы не будем подробно рассматривать unique_ptr, т.к. Стандарт C++0x ещё не завершён, а его реализации если и существуют, то не распространены широко.

Другие реализации

Список реализаций умных указателей не ограничивается перечисленными выше классами. Помимо них, существуют ещё реализации в библиотеках Loki, Poco, STLSoft и многие, многие другие. Их семантика и поведение зачастую схожее с описанными выше реализациями, но в некоторых случаях может сильно отличаться. Подробные описания этих реализаций приводятся в документации на соответствующие библиотеки.

О чём следует помнить, используя умные указатели

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

Циклические ссылки возникают в том случае, если 2 класса напрямую или косвенно ссылаются друг на друга, используя умные указатели:

      class B;

class A
{

// ...private:
  std::auto_ptr<B> b_;
};

class B
{
// ...private:
  std::auto_ptr<A> a_;

};

В таком случае, если мы имеем экземпляры классов A и B, ссылающиеся друг на друга, при попытке удалить один из них произойдёт зацикливание. Пример для auto_ptr выглядит надуманным, более реальный пример – с использованием shared_ptr:

      class B;

class A
{
// ...private:
  std::tr1::shared_ptr<B> b_;
};

class B
{
// ...private:
  std::tr1::shared_ptr<A> a_;
};

В этом случае зацикливания не произойдёт, однако случится утечка памяти – счётчики ссылок обоих объектов никогда не обнулятся, соответственно и деструкторы вызваны не будут. Следует помнить, что умные указатели не решают проблемы циклических ссылок, ответственность в этом случае целиком лежит на программисте.

Вторая распространённая ошибка – это совместное использование обычных (raw) и умных указателей. Проблема в том, что переходя от умного указателя к обычному, вы сразу же нарушаете контракт владения умного указателя, он уже не контролирует экземпляр объекта, а это может привести к печальным последствиям:

std::auto_ptr<SomeClass> a(new SomeClass);
// ...
SomeClass* ptr = a->get();
// ...
std::auto_ptr<SomeClass> b(ptr); // Ай!!!

В приведённом выше примере экземпляр объекта будет удалён дважды, а это приведёт скорее всего к ошибке доступа к памяти. Следует всегда помнить, что, переходя от умного указателя к обычному, нужно обнулить умный указатель:

SomeClass* ptr = a->release();

Заключение

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

Источники мудрости

  1. C++ Standard, International Standard ISO/IEC 14882
  2. Дьюхерст, С. C++. Cвященные знания – М. : Символ-плюс, 2007. – 240 с.
  3. Мейерс, С. Эффективное использование C++, 55 верных советов улучшить структуру и код ваших программ – М. : ДМК пресс, 2006. – 300 с.
  4. Уилсон, М. C++. Практический подход к решению проблем программирования – М. : КУДИЦ-Образ, 2006. – 736 с.
  5. Джосьютис, Н. C++. Стандартная библиотека – СПб. : Питер, 2004. – 736 с.
  6. ISO/IEC TR 19768: C++ Library Extensions TR1

Благодарности

Большое спасибо Валерию Артюхину, благодаря чьей критике эта статья стала лучше.


Эта статья опубликована в журнале RSDN Magazine #1-2008. Информацию о журнале можно найти здесь