Search
Write a publication
Pull to refresh

Забудьте о проблемах с памятью!

Все программисты на C++ знают, что такое memory leak и сколько сил требует борьба с этим недугом, если он вдруг появился. Спасение ищется во всевозможных smart pointer-ах – объектах, которые автоматически управляют памятью, освобождая ее в тот момент, когда объект становится недоступен.
Основная проблема этих оберток – они не универсальны, и каждый раз для нового объекта приходится писать уже надоевшие конструкторы копирования, перекрывать операторы и заниматься прочей неблагодарной и черной работой.

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

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

Особенно полезна эта статья будет тем людям, которые, разрабатывая приложения на 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&gt 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, что соответствующим образом снижает производительность, однако данное решение и не ориентировано на максимальную производиельность – она ориентирована на максимальное удобство, отвлечение от проблем управления памятью и сосредоточение на разработке приложения.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.