вторник, 21 февраля 2012 г.

Неглупые указатели (smart_pointers)

Студент увидел как-то на рынке, что какой-то мужик 
продает мозги разных специалистов. Заинтересовался, 
подходит, спрашивает что почем. Ему объясняют: 
«Это мозги математика, они по сто рублей за килограмм, 
это – мозги физика, они – по двести за кило, а вот эти
 – мозги философа, они самые дорогие, по две тысячи». 
Студент проникся уважением, спрашивает: 
«Что, философские мозги и правда такие хорошие?» 
«Да нет, дело не в этом, просто представь, это же 
сколько философов отловить надо, чтобы набрать 
целый килограмм мозгов!»


  Прочитав Скотта Майерса, я стала противником auto_ptr. Auto_ptr - это зло-зло! Я об этом сказала своему начальнику. Он же сделал круглые глаза и выразил удивление, чем меня сильно озадачил. Я опять повторила: "Зло-Зло!". А он: "Почему?". Когда я объяснила, почему зло, он сделал простейший вывод: "Просто не надо их использовать в контейнерах." Действительно, подумала я... :) 

  И так небольшой обзор умных указателей: 

 STL: 
  • auto_ptr - только не для использования в контейнерах!! (См. Скотт Майерс, "Effective STL", совет №8)
  • weak_ptr - (доступен только в ISO/IEC 14882:2011, или при указании опции компиляции -std=c++0x) 
  • shared_ptr - (доступен только в ISO/IEC 14882:2011, или при указании опции компиляции -std=c++0x) 

   И вообще, в файлы с новым стандартом лучше не заглядывать - незаметно сносит крышу. То есть вроде бы всё на месте, а холодком веет. Необычные конструкции вроде таких, пугают:  shared_ptr(shared_ptr&& __r), но об этом можно почитать здесь.

 Boost: 
  • scoped_ptr - самый простой умный указатель. Как написано в комментариях - simple solution for simple needs. Умеет возвращать память при выходе из области видимости (в деструкторе). Умеет делать get(), reset(), swap();
  • intrusive_ptr - умный указатель со встроенным счетчиком ссылок. Может быть создан из произвольного raw-указателя (сырого указателя) типа T*. Блок памяти для intrusive_ptr такой же, как и для соответствующего raw-указателя. Умеет делать get(), reset(), swap(). Но для него придется определить функции подсчета ссылок и освобождения памяти. В конструкторе intrusive_ptr( T * p, bool add_ref = true ) вторым параметром (как это ясно из названия) можно повлиять на увеличение счетчика в первый раз. Его удобно использовать при разработке, где требуется самому определять не только, как освободить память, но и как работает счетчик ссылок. Главное правильно, если не ясно какой из двух использовать: его или shared_ptr, попробуйте использовать сначала shared_ptr;
  • weak_ptr - слабенький указатель :). Используется совместно с  shared_ptr. Хранит ссылку на объект, которым уже владеет shared_ptr. Для доступа к хранимому объекту нужно перейти к shared_ptr, используя соответствующий  конструктор: template<class Y> explicit shared_ptr(weak_ptr<Y> const & r). Есть внутренние переменные: указатель на данные, счетчик ссылок (boost::detail::weak_count). Умеет делать lock() - защита от удаления объекта и сказать, что объект уже никем не используется с помощью функции expired();
  • shared_ptr - умный указатель посложнее. Есть внутренние переменные - указатель на данные, счетчик ссылок (boost::detail::shared_count). Его использование гарантирует, что объект будет удален, когда последний shared_ptr, указывающий на него, уничтожится или сбросится (reset()).


  shared_ptr в конструкторе можно передать функцию удаления для хранимого в shared_ptr объекта: shared_ptr(ptr, deleter). Тогда при удалении будет вызван deleter(ptr): 

#include <iostream>
#include <boost/shared_ptr.hpp>

struct A{
  A(){
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }
  ~A(){
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }
};

struct myDeletor{
  void operator()(const A* p){
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    delete p;
  }
};

void deletion(){
  boost::shared_ptr<A> ptr(new A, myDeletor());
}

int main(){
  deletion();
  return 0;
}
   Можно передать ещё и аллокатор. О как! Круто! Он, наверное, будет выделять память для нашего объекта, наивно подумала я. Но здравый смысл потряс меня за плечи и ткнул носом в new A. Стоп! А как это? Мы же уже выделили память для объекта:

 boost::shared_ptr<A>(new A, myDeletor(), myAllocator()).

   Что же будет делать myAllocator? А, может быть, там извращение (которое сразу пришло мне в голову, слабонервным не читать). myDeletor() сразу удаляет new A, а myAllocator выделяет память заново?
   Неееее! Там другое, myAllocator и не чешется выделять память под Ваш любимый объект, он ведет себя очень странно. Он выделяет память для счетчика ссылок, точнее для его имплементации (sp_counted_impl_pda). Опа! "Такой удар со стороны классика! А?". Смотрим в документацию. В документации написано, что конструктор:


template<class Y, class D, class A> shared_ptr(Y * p, D d, A a);


выделяет память, используя копию a ("constructor allocates memory using a copy of a"), но тут и слова нет про то, для кого он выделяет память. "Я стоял перед этими гирями и безумно хохотал", вот как надо хитро писать, не придерешься. И нет никаких противоречий, а выделять память с помощью нашего аллокатора он будет для своих внутренних структур. myDeletor, как и в предыдущем варианте, вызовется при удалении A. А myAllocator для правильной работы должен удовлетворять стандартным требованиям для аллокаторов C++, поэтому у него должны быть определены и allocate() и deallocate() и ещё загадочная структура rebind: 

template<class T>
struct myAllocator{

  myAllocator(){
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }

  template<class U>
  myAllocator(const myAllocator<U>&){
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }
  
  ~myAllocator(){
     std::cout << __PRETTY_FUNCTION__ << std::endl;
  }
    
  template<class U> struct rebind {
    typedef myAllocator<U> other; 
  };
  
  T* allocate(size_t n, const void* = 0){ 
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    return static_cast<T*>(::operator new(n * sizeof(T))); 
  }
  
  void deallocate(T* p, size_t num){ 
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    ::operator delete(static_cast<void*>(p)); 
  }
};

void deletion_allocation(){
  boost::shared_ptr<A> ptr(new A, myDeletor(), myAllocator<A>());
  // std::shared_ptr<A> ptr(new A, myDeletor(), myAllocator<A>());
}

int main(){
  deletion_allocation();
  return 0;
}

А вот, что происходит внутри (рассмотрим вариант, не бросающий исключения) /usr/include/boost/smart_ptr/detail/shared_count.hpp:145:
template<class P, class D, class A> shared_count( P p, D d, A a ): pi_( 0 )
 {
    typedef sp_counted_impl_pda<P, D, A> impl_type;  // Вот она имплементация
    typedef typename A::template rebind< impl_type >::other A2; // А это наш класс аллокатор,
                                  // с шаблоном impl_type, то есть это myAllocator<impl_type>

    A2 a2( a ); // Вот она копия 'a' - никто не наврал :)

    try
    { 
       // А вот и использование нашего аллокатора
       pi_ = a2.allocate( 1, static_cast< impl_type* >( 0 ) );  
       new( static_cast< void* >( pi_ ) ) impl_type( p, d, a ); 
     }
     catch(...)
     {
       d( p ); // Вызываем myDeleter() для нашего объекта
       if( pi_ != 0 ) 
       {
          // Если удалось выделить память для имплементации, освобождаем с помощью нашего аллокатора
          a2.deallocate( static_cast< impl_type* >( pi_ ), 1 );
        }
          throw;
        }
 }

    Во! Как бывает, так что читай - не читай документацию, всё равно надо смотреть в корень, как учит Козьма Прутков.

   Всё это актуально и для std::shared_ptr, но он будет действовать только в новом стандарте.
   P.S. Кстати, не забудем об умных массивах:  shared_array, scoped_array.

Комментариев нет: