пятница, 18 ноября 2011 г.

Особенности использования shared_ptr и RefCounter

Бредисловие
Думаю каждый программист слышал или использовал shared pointers и встречался с идиомой ref counter для разделяемых ресурсов. По отдельности их использование не представляет сложности и опасности, но что может быть если совместить их вместе?

А как все начиналось...
Предположим мы расширяем существующий функционал программы. Архитектура программы содержит в себе некий "промежуточный" слой service, который состоит из классов-менеджеров. Т.е. для записи файла на CD есть класс CdWriterManager, для записи файлов на расшаренные папки ShareWriterManager и прочие. И вот нам дают задание написать класс для записи файлов на FTP.

Следуя уже сложившейся традиции, мы пишем следующий класс:
  1. class FTPWriterManager  
  2. {  
  3. public:  
  4.   FTPWriterManager(){}  
  5.   virtual ~FTPWriterManager(){}  
  6.   void WriteFile(IFile::Ptr file)  
  7.   {  
  8.     //some logic here  
  9.   }  
  10.   typedef std::tr1::shared_ptr<FTPWriterManager> Ptr;  
  11. };  
  12.   
  13. //get new instance of manager(not singleton!)  
  14. FTPWriterManager::Ptr GetFtpWriterManager();  

Как видно из кода, есть глобальная функция, возвращающая новый экземпляр класса. Т.е. в разный потоках могут быть разные объекты. И вот вы тестируете класс, и проверив, что все работает, гордые собой, отправляетесь "индексировать" Хабр :-)

Но тут выясняется, что наш класс должен как-то реагировать на внешние сигналы, или получать данные из других частей программы с помощью callbacks. Все callbacks классы в программе унаследованы от RefCounter.
  1. class RefCounter  
  2. {  
  3. private:  
  4.   volatile long Counter;  
  5.   
  6. public:  
  7.   RefCounter() : Counter(1)  
  8.   {  
  9.   }  
  10.     
  11.   virtual ~RefCounter()  
  12.   {  
  13.   }  
  14.     
  15.   virtual void AddRef()  
  16.   {  
  17.     atomic_increment(&Counter);  
  18.   }  
  19.     
  20.   virtual void Release()  
  21.   {  
  22.     if (atomic_decrement(&Counter) == 0)  
  23.     {  
  24.       delete this;  
  25.     }  
  26.   }  
  27. };  
  28.   
  29. class SomeHandler : public RefCounter  
  30. {  
  31. public:  
  32.   virtual void OnStart() = 0;  
  33.   virtual void OnFinish() = 0;  
  34. };  

После этого есть два пути: написать отдельный callback, унаследовав его от RefCounter, и сохранить объект этого типа в нашем менеджере. Или еще один "гениальный" способ: воспользоваться наследованием, дабы не плодить лишние классы и объекты

Поворот не туда
"Вот говорили мне - нельзя с утра до ночи смотреть телевизор - вот вам, пожалуйста"(с)

Да, да, сколько раз уже предупреждали о том, чтобы отдавать предпочтение композиции наследованию, но так ведь проще, и класс лишний писать не недо:
  1. class FileManager  
  2. {  
  3. public:  
  4.   ~virtual FileManager()  
  5.   {  
  6.     Handler->Release();  
  7.   }  
  8.     
  9.   void SetCallback(SomeHandler* handler)  
  10.   {  
  11.     Handler = handler;  
  12.   }  
  13. private:  
  14.   SomeHandler* Handler;  
  15. };  
  16.   
  17. class FTPWriterManager : public SomeHandler  
  18. {  
  19. public:  
  20.   FTPWriterManager(){}  
  21.   virtual ~FTPWriterManager(){}  
  22.   void WriteFile(IFile::Ptr file)  
  23.   {  
  24.     //some logic here  
  25.   }  
  26.     
  27.   void OnStart()  
  28.   {  
  29.     //...  
  30.   }  
  31.     
  32.   void OnFinish()  
  33.   {  
  34.     //...  
  35.   }  
  36.     
  37.   void Initialize()  
  38.   {  
  39.     FileManagerObj->SetCallback(this);  
  40.   }  
  41.   typedef std::tr1::shared_ptr<FTPWriterManager> Ptr;  
  42. private:  
  43.   std::tr1::shared_ptr<FileManager> FileManagerObj;  
  44. };  

И после этого вы отдаете класс некоему менеджеру, обрабатываете события в функциях OnFinish, OnStart  и все вроде работает. Но! Если объект FTPWriterManager будет разрушен, то случится беда.

А вот и Джонни...
Первым вызовется деструктор ~FTPWriterManager. В момент уничтожения FTPWriterManager будет вызван деструктор класса SomeEventHandler, и объект будет разрушен, (причем RefCounter::Counter не будет равен 0). Затем начнет уничтожаться объект FileManagerObj, который попытается уничтожить Handler, вызвав Release, но Handler уже указывает на не валидную память.

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

Вывод
А вывод довольно прост: предпочитать композицию наследованию. Ну и почаще использовать assert :)

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

Отправить комментарий