Все программисты на C++ знают, что такое memory leak и сколько сил требует борьба с этим недугом, если он вдруг появился. Спасение ищется во всевозможных smart pointer-ах – объектах, которые автоматически управляют памятью, освобождая ее в тот момент, когда объект становится недоступен.
Основная проблема этих оберток – они не универсальны, и каждый раз для нового объекта приходится писать уже надоевшие конструкторы копирования, перекрывать операторы и заниматься прочей неблагодарной и черной работой.
Статья рассчитана на программистов, которые уже освоили С++, но еще не выработали собственных рабочих методов борьбы с утечками памяти. Методы могут быть разные, однако большинство из них требуют определенной аккуратности и методичности в выделении и освобождении памяти.
Не лучше ли приучать программистов к аккуратности, а не перекладывать проблему с больной головы на здоровую? Оставим этот вопрос философам. В нашем мире многое (в том числе размер зарплаты) решает эффективность работы программиста, а не его религиозные убеждения об абсолютном вреде смартпойнтеров.
Особенно полезна эта статья будет тем людям, которые, разрабатывая приложения на C# (а написание приложений со сложной бизнес-логикой эффективнее просто ввиду того, что C# позволяет практически сразу сосредоточиться на логике работы приложения, а не на проблемах указателей и кодировок), уткнулись в то, что шарп не все может, и некоторые вещи приходится писать на «плюсах». Я в таких случая использую связку из «плюсовых» ActiveX, которые подключаются через стандартный для C# механизм COM Interop.
C# — поганый язык. Поганый он тем, что хороший, а к хорошему быстро привыкаешь. После полугода работы на C# понять «плюсовую» логику становится очень тяжело. В таком случае, можно принести капельку «шарпа» в «плюсы» — делать memory-safe объекты и работать с ними, используя «шарповый» синтаксис — без амперсандов и звездочек.
Корневым объектом нашей memory-safe иерархии станет объект с красноречивым названием __object. Все, что у него есть – это внутренний счетчик ссылок и две функции с не менее красноречивыми названиями — __addref и __release.
Теперь, унаследовав любой класс от __object, мы получим объект, способный автоматически удаляться в момент, когда обнуляется его внутренний счетчик ссылок. Это – первый шаг к будущему величию.
Однако, несмотря на подсчет ссылок, мы все еще вынуждены вручную делать __addref и __release для корректной работы этого механизма. Избавить нас от этой рутины призван класс-обертка __objectPtr. Он конечно не так тривиален, но тоже несложен.
Что это дает. А дает это вот что.
Определив теперь класс как наследник от __object и создав с помощью typedef обертку классом __objectPtr, мы получаем отсутствие геморроя с передачей указателей или ссылок в функцию а также возврата значений из функции. Избавляемся от необходимости следить, где мы выделяем память и где должны ее освободить.
Смотрите сами.
Конечно, в случае сложной иерархии следует использовать виртуальное наследование от __object, что соответствующим образом снижает производительность, однако данное решение и не ориентировано на максимальную производиельность – она ориентирована на максимальное удобство, отвлечение от проблем управления памятью и сосредоточение на разработке приложения.
Основная проблема этих оберток – они не универсальны, и каждый раз для нового объекта приходится писать уже надоевшие конструкторы копирования, перекрывать операторы и заниматься прочей неблагодарной и черной работой.
Статья рассчитана на программистов, которые уже освоили С++, но еще не выработали собственных рабочих методов борьбы с утечками памяти. Методы могут быть разные, однако большинство из них требуют определенной аккуратности и методичности в выделении и освобождении памяти.
Не лучше ли приучать программистов к аккуратности, а не перекладывать проблему с больной головы на здоровую? Оставим этот вопрос философам. В нашем мире многое (в том числе размер зарплаты) решает эффективность работы программиста, а не его религиозные убеждения об абсолютном вреде смартпойнтеров.
Особенно полезна эта статья будет тем людям, которые, разрабатывая приложения на C# (а написание приложений со сложной бизнес-логикой эффективнее просто ввиду того, что C# позволяет практически сразу сосредоточиться на логике работы приложения, а не на проблемах указателей и кодировок), уткнулись в то, что шарп не все может, и некоторые вещи приходится писать на «плюсах». Я в таких случая использую связку из «плюсовых» ActiveX, которые подключаются через стандартный для C# механизм COM Interop.
C# — поганый язык. Поганый он тем, что хороший, а к хорошему быстро привыкаешь. После полугода работы на C# понять «плюсовую» логику становится очень тяжело. В таком случае, можно принести капельку «шарпа» в «плюсы» — делать memory-safe объекты и работать с ними, используя «шарповый» синтаксис — без амперсандов и звездочек.
Корневым объектом нашей memory-safe иерархии станет объект с красноречивым названием __object. Все, что у него есть – это внутренний счетчик ссылок и две функции с не менее красноречивыми названиями — __addref и __release.
class __object
{
private:
long m_dwRef;
public:
__object(void) { m_dwRef = 0; }
virtual ~__object(void) { }
inline long __addref(void) { return ++m_dwRef; }
long __release(void) { if(--m_dwRef == 0) delete this; return m_dwRef; }
};
Теперь, унаследовав любой класс от __object, мы получим объект, способный автоматически удаляться в момент, когда обнуляется его внутренний счетчик ссылок. Это – первый шаг к будущему величию.
Однако, несмотря на подсчет ссылок, мы все еще вынуждены вручную делать __addref и __release для корректной работы этого механизма. Избавить нас от этой рутины призван класс-обертка __objectPtr. Он конечно не так тривиален, но тоже несложен.
template <class T> class __objectPtr
{
private:
T* lp_data;
public:
__objectPtr(void) { lp_data = new T(); lp_data->__addref(); }
__objectPtr(T* src) { lp_data = src; lp_data->__addref(); }
__objectPtr(__objectPtr& src) { lp_data = src.lp_data; lp_data->__addref(); }
~__objectPtr(void) { if(lp_data) lp_data->__release(); }
T* Detach()
{
T* retval = lp_data;
lp_data = NULL;
return retval;
}
inline static void Release(T*& target) { target->__release(); target = NULL; }
inline T* operator -> () { return lp_data; }
inline T* operator & () {return lp_data; }
__objectPtr& operator = (__objectPtr& src)
{
if(lp_data != NULL)
lp_data->__release();
lp_data = src.lp_data;
lp_data->__addref();
}
__objectPtr& operator = (T* src)
{
if(lp_data != NULL)
lp_data->__release();
lp_data = src;
lp_data->__addref();
}
Что это дает. А дает это вот что.
Определив теперь класс как наследник от __object и создав с помощью typedef обертку классом __objectPtr, мы получаем отсутствие геморроя с передачей указателей или ссылок в функцию а также возврата значений из функции. Избавляемся от необходимости следить, где мы выделяем память и где должны ее освободить.
Смотрите сами.
class TestClass : public __object
{
public:
TCHAR* longString;
void CreateString(const TCHAR* string)
{
int strsize = wcslen(string) + 1;
longString = new TCHAR[strsize];
wcscpy_s(longString, strsize, string);
}
};
typedef __objectPtr<TestClass> SMART_TestClass;
SMART_TestClass CreateSmart2(SMART_TestClass src)
{
SMART_TestClass retval = new TestClass();
retval->CreateString(src->longString);
return retval;
}
int _tmain(int argc, _TCHAR* argv[])
{
// Практически C# :)
SMART_TestClass a = new TestClass();
// Теперь у нас в куче лежит экземпляр класса TestClass, который знает, что к нему ведет одна smart-ссылка.
// Благодаря переопределению оператора, мы даже не замечаем, что работаем на самом деле с оберткой
a->CreateString(_T("Wow, this is string!"));
// Объект по-прежнему один, но ссылки уже две.
SMART_TestClass b = a;
// Если мы переопределим оператор == для нашего TestClass - при сравнении оберток будет вызываться именно он.
// Если же нет - то просто сравниваются указатели.
if(b == a)
// Здесь наш объект получит еще одну ссылку
SMART_TestClass c = b;
// Но потом сразу ее потеряет из-за вызова деструктора при выходе из блока.
// А теперь смотрите, как мы передаем в функцию объекты. Не указатели и не ссылки - объекты!
// Благодаря тому, что наша smart-обертка - это простой указатель, нам не нужно передавать ссылку или указатель
// на него - конструктор копирования отработает автоматически.
SMART_TestClass d = CreateSmart2(b);
// Также мы можем не думать, как возвращать значение из функции - объект размещается в куче и лежит там, пока
// он хоть кому-то нужен.
// А если вдруг он окажется не нужен - то он сам умрет.
CreateSmart2(b);
// Несмотря на то, что строки в объектах одинаковые - сами объекты разные. Однако, если бы у нас был
// переопределен оператор == на сравнение строк - здесь бы программа и заканчивалась.
if(d == b)
return 1;
// Detach пригодится, если нам надо передать указатель во внешнюю среду (например, если его надо вернуть в
// in-proc com)
TestClass * detached = d.Detach();
SMART_TestClass e = detached;
SMART_TestClass::Release(detached);
return 0;
}
Конечно, в случае сложной иерархии следует использовать виртуальное наследование от __object, что соответствующим образом снижает производительность, однако данное решение и не ориентировано на максимальную производиельность – она ориентирована на максимальное удобство, отвлечение от проблем управления памятью и сосредоточение на разработке приложения.