4

SmartPointers для C++ в стиле Java

Не так давно я перешел с плюсов на Java. Согласен с автором поста в том, что чисто по-человечески писать на Java приятней, чем на C++. Страхов меньше, язык проще, в стандартной библиотеке есть почти все, что нужно. Одно мне не нравится - все же для любой проги на Java необходимо окружение, а 17 МБ (для JRE 5) не каждый готов скачать ради того, чтобы запустить простенькую програмку. Хочется мне иметь в C++ безопасность и удобство Java.
В процессе мозголомства и раздумий на тему - как же в принципе реализуется сборщик мусора( он же garbage collector - GC) и лазанью по инету пришел к одному результату... А именно: в C++ можно создать SmartPointer, пользоваться которым можно абсолютно так же, как в Java пользуются ссылками на объекты. И поведение его будет таким же, как поведение ссылок в Java. При этом GC как таковой даже и не нужен - его функции будет выполнять сам SmartPointer.

Как ведут себя ссылки в Java? Через них можно обращаться к полям/методам класса, присваивать ссылкам другие ссылки совместимых типов, выполнять проверку на равенство, делать ссылку "инвалидной" (т.е. присвоить ей значение null). По выходе ссылки из области видимости память, связанная с этой сылкой, может быть освобождена сборщиком мусора, при условии что эта ссылка не была передана куда-то еще. Вот, пожалуй и все.
Итак, если в нашей программе будут только такие SmartPointer-ы и не будет других видов указателей, про ручное освобождение памяти и утечку памяти можно будет забыть. Конечно, можно умудрится сделать циклические ссылки (и на Java это сделать проще, чем на C++) В таких случаях, конечно, придется освобождать память вручную, но не через delete, а просто присваивая ссылке значение NULL, что гораздо безопаснее (можно присваивать одной и той же ссылке NULL сколько влезет, и не будет ничего страшного).
Что бы быть подобием java-ссылки, наш SmartPointer должен уметь:
-выполнять операцию присваивания таких же SmartPointer-ов или SmartPointer-ов совместимых типов,
-выполнять операцию присваивания значения NULL,
-выполнять операцию сравнения.
-знать, сколько его копий существует на данный момент.
При всем этом не должно происходить утечек памяти. То есть если один указатель перезаписывается другим, то память на которую указывал "затертый" указатель, должна быть освобождена.
Объявление этого SmartPointer-a я изобразил так:
template<class T>
class AkSmartPointer{
private:
int *ptrCount;
T* ptrObject;
//Удаление 1 экземпляра ССЫЛКИ:
void removeRef();
public:
//Конструкторы:
//"По умолчанию":
AkSmartPointer();
//При создании экземпляра объекта:
AkSmartPointer(T *object);
//Копирование:
AkSmartPointer(const AkSmartPointer<T> &sp);
//Оператор присваивания:
AkSmartPointer<T> & operator=(const AkSmartPointer<T> &s;sp);
//Оператор проверки на равенство:
bool operator == (const AkSmartPointer<T> &sp) const;
//Удаление 1 экземпляра ССЫЛКИ:
~AkSmartPointer();
//Поучение ссылки на объект:
T * operator ->();
};
Вроде ничего не забыл :) Получение указателя необходимо для доступа к интерфейсу указуемого объекта. Реализацию такого SmartPointer-а я сделал вот такой:

class AkSmartPointerException{
public:
AkSmartPointerException( const char *msg, const char *fname, int line){
printf("Error: %s at %s:%i\n", msg, fname, line);
}
};
template<class T>
class AkSmartPointer{
private:
int *ptrCount;
T* ptrObject;
public:
//Конструкторы:
//"По умолчанию" просто обнуляем все указатели:
AkSmartPointer(){
ptrCount = NULL;
ptrObject = NULL;
}
//При создании экземпляра объекта:
AkSmartPointer(T *object){
ptrObject = object;
if(ptrObject != NULL){
ptrCount = new int(1);
printf("New reference 0x%X created.\n", (unsigned)ptrObject);
}else
ptrCount = NULL;
}
//Копирование:
AkSmartPointer(const AkSmartPointer<T> &sp){
ptrCount = sp.ptrCount;
ptrObject = sp.ptrObject;
if(ptrCount != NULL){
(*ptrCount)++;
printf("New copy of reference 0x%X created, %i references total\n",
(unsigned)ptrObject, *ptrCount);
}
}
//Оператор присваивания:
AkSmartPointer<T> & operator=(const AkSmartPointer<T> &sp){
//Если данный объект уже равен присваемому:
if(sp.ptrCount == ptrCount && sp.ptrObject == ptrObject)
return *this;// ничего не делаем, просто возвращаем текущее значение.
//если выполняется только одно условие из двух - то происходит нечто странное,
//и мы выкидываем исключение:
else if(sp.ptrCount == ptrCount || sp.ptrObject == ptrObject)
throw AkSmartPointerException("ptrCount or ptrObject is equal in different smart pointers", __FILE__, __LINE__);
removeRef();//удаляем у текущего объекта ссылку
ptrCount = sp.ptrCount;
if(ptrCount != NULL)
(*ptrCount)++;
ptrObject = sp.ptrObject;
if(ptrObject != NULL && ptrCount != NULL)
printf("New copy of reference 0x%X created, %i references total\n",
(unsigned)ptrObject, *ptrCount);
return *this;
}
//Оператор проверки на равенство:
bool operator == (const AkSmartPointer<T> &sp) const{ return ptrObject == sp.ptrObject; }
//Удаление 1 экземпляра ССЫЛКИ:
~AkSmartPointer(){removeRef(); }
//Получение ссылки на объект:
T * operator ->(){
if(ptrCount != NULL)
return ptrObject;
throw AkSmartPointerException("Null pointer dereferencing", __FILE__, __LINE__);
}
//Удаление 1 экземпляра ССЫЛКИ:
private:
void removeRef(){
if(ptrCount != NULL){
(*ptrCount)--; //Если ссылок не осталось - зануляем все остальное:
if(*ptrCount <= 0){
unsigned x = (unsigned)ptrObject;
delete ptrCount;
if(ptrObject != NULL)
delete ptrObject;
printf("All references 0x%X deleted.\n", x);
}else
printf("One reference 0x%X removed, %i remaining\n",
(unsigned)ptrObject, *ptrCount);
ptrCount = NULL;
ptrObject = NULL;
}
else if(ptrObject != NULL)//если ptrCount == NULL, но ptrObject != NULL
throw AkSmartPointerException("ptrCount == NULL, but ptrObject != NULL", __FILE__, __LINE__);
else
printf("Do nothing - ptrCount and ptrObject is null.\n");
}
};
Наверное, все понятно и так, но немножко комментариев напишу. Каждое действие по созданию или удалению ссылок на объект я сопровождаю выводом на экран, дабы было видно, какие функции-члены SmartPointer-а вызываются при манипуляциях с ним, отсюда куча printf().

В операторе присваивания сначала проверяем, не присваиваем ли мы нашей ссылке ее же значение. Если да - число ссылок не изменилось и ничего делать не надо. Если нет (не выполняются оба условия sp.ptrCount == ptrCount и sp.ptrObject == ptrObject, либо лишь одно из них), то проверяем: оба условия не выполняются или только одно из них. Если только одно условие не выполняется-например, sp.ptrCount == ptrCount, но sp.ptrObject != ptrObject, - это значит, что ссылки на разные объекты имеют общий счетчик, а это недопустимо. Таким образом, в случае выполнения только одного условия из двух, сообщаем об ошибке. Если оба условия не выполняются, то все ОК - одна ссылка перезаписывается другой. В результате присваивания одна из ссылок должна быть удалена (та, что стояла в левой части выражения присваивания) и появится еще одна новая ссылка (та, что стояла в правой части этого выражения). Для удаления ссылки вызываем функцию removeRef у объекта-владельца этой ссылки.

Все остальное по-моему достаточно очевидно (если честно, упарился описывать :) ). Эту реализацию SmartPointer я разместил в файле refCount.hpp.

Уфф, добрались до самого интересного (говорят, русским программистам всегда не терпится запустить программу и увидеть ее в работе. Это правда :) ). Написал я тест и запустил, скрестив пальцы под столом. Сначала хотел его целиком тут привести, но потом передумал. Тест просто показал, что такой SmartPointer вроде бы работает именно так, как я ожидал. Приведу здесь только ту часть теста, которая показала, что происходит при создании объектов и как работает vector.
#include <vector>
#include "refCount.hpp"
struct TestObject{
double x, y;
TestObject(double _x, double _y): x(_x), y(_y){}
};
using namespace std;
typedef AkSmartPointer<TestObject> TestObjectSmartPtr;
int main(int n, char **args){
printf("Declare vector<TestObjectSmartPtr>:\n");
vector<TestObjectSmartPtr> refvect;
{ //Начало блока.
printf("Declare objects a, b, c:\n");
AkSmartPointer<TestObject> sp_a, sp_b, sp_c;
printf("Create object a:\n");
sp_a = new TestObject(1.0, 1.0);//всего 1 ссылка на a
.....
} //Конец блока.
Этот участок кода дал мне такую вот распечатку:
Declare vector:
Declare objects a, b, c:
Create object a:
New reference 0x3F3C90 created.
Do nothing - ptrCount and ptrObject is null.
New copy of reference 0x3F3C90 created, 2 references total
One reference 0x3F3C90 removed, 1 remaining
Хорошо видно, какую последовательность вызовов порождает выражение

sp_a = new TestObject(1.0, 1.0);//всего 1 ссылка на a
Расписывать как все происходит, не буду, дабы не утомлять читателей. :)

Теперь - о том,что касается работы vector. Свой тест я откомпилил в MinGW версии 3.4.2, в Borland C++ builder 5 и в MSVC 6.0 c подключенным компилятором IA-32 v5.0. Что касается создания объектов, и в том и в другом случае распечатки были почти идентичны (за исключением значений адресов объектов). Но что касается vector - разница заметна. В моем тесте есть такой кусок:

printf("Addition object a:\n");
refvect.push_back(sp_a);//2 ссылки
printf("Addition object b:\n");
refvect.push_back(sp_b);//2 ссылки
printf("Addition object c:\n");
refvect.push_back(sp_c);//2 ссылки
Откомпилированный под MinGW, этот кусок вывел на экран следующее:
Addition object a:
New copy of reference 0x3F3C90 created, 2 references total
Addition object b:
New copy of reference 0x3F3C90 created, 3 references total
New copy of reference 0x3F23B8 created, 2 references total
One reference 0x3F3C90 removed, 2 remaining
Addition object c:
New copy of reference 0x3F3C90 created, 3 references total
New copy of reference 0x3F23B8 created, 3 references total
New copy of reference 0x3F23E8 created, 2 references total
One reference 0x3F3C90 removed, 2 remaining
One reference 0x3F23B8 removed, 2 remaining

То есть каждый раз при добавлении 1 элемента в конец вектора, под весь вектор выделялся новый участок памяти, туда копировались элементы из "старого" вектора, а старый участок памяти удалялся(!). То же самое в версии STL MSVC 6.0 (но это ладно, она очень древняя). Похоже, в Borland-е все сделано куда разумнее. Откомпилированный в Borland-е тот же кусок программы вывел на экран гораздо меньше сообщений:
Addition object a:
New copy of reference 0x65415C created, 2 references total
Addition object b:
New copy of reference 0x654180 created, 2 references total
Addition object c:
New copy of reference 0x6541A4 created, 2 references total

Как видно, реализация vector от Borland все же лучше аналогичной в MinGW. Похоже, у Borlan-да при создании вектора сразу резервируется блок под несколько элементов.
Ну и под конец - один из способов, как приведенную реализацию SmartPointer-а заставить работать неправильно:
AkSmartPointer a = new Object(...);
Object *b = a.operator->();
delete b;//пипец!

Но имхо, это будет уже маразм ради маразма :) Если знаете, как с таким SmartPointer-ом организовать утечку памяти - плиз, кидайте в комменты примерчик, но только без описанного маразма или ему подобных (желательно программу целиком). Циклические ссылки приветствуются, но желательно, что бы Ваш пример был компилируемым и запускаемым :). К моему стыду, я до сих пор не знаю, как циклические ссылки можно организовать в C++ (мне один раз это было надо, но я их реализовал так криво, что тошно вспоминать).
P.S. Мне сложно было реализовать ситуацию, когда 2 объекта разных типов должны были ссылаться друг на друга.

4 коммент.:

sash_ko комментирует...

можно сразу взять удобный готовый смартпоинтер в бусте - shared_ptr. тем более, что он включен в новый стандарт с++ ;)
более того, shared_ptr можно использовать на пару с weak_ptr, что бы бороться с циклическими ссылками. пример циклических ссылок привести трудно, но они существуют :)

Lotrex комментирует...

Да, не спорю - boost::shared_ptr очень похож на тот smartpointer, что я описал. Проблема в том, что shared_ptr нет в старом стандарте C++, а когда появятся компиляторы, поддерживающие новый стандарт - не известно. К тому же мне нужны smartpointerы, полностью имитирующие ссылки в Java. Выражается это в том, что такие smartpointerы должны допускать проверку на равенство с NULL и должны при присваивании им NULL уменьшать счетчик ссылок на 1. При этом оператор доступа по индексу []мне не нужен.

Анонимный комментирует...

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

shared_ptr допускает проверку на равенство с NULL, вот только присваивать NULL ему нельзя. А оператора [] у него нет.

Кстати, я не агитирую использовать boost::shared_ptr - это я просто напомнил о его существовании.

Lotrex комментирует...

То, что shared_ptr нет в старом стандарте C++ на самом деле не проблема. Если не хочется ради одного смартпоинтера подключать весь boost, можно воспользоваться утилиткой, которая сама "выдерет" нужный код со всеми зависимостями.
Это замечательно! :)
shared_ptr допускает проверку на равенство с NULL, вот только присваивать NULL ему нельзя. А оператора [] у него нет.
К сожалению, Boost мне пока использовать не приходилось, как-то обходился STL-ом. Поэтому я (к своему стыду :-[) не больно хорошо знаю те smartpoinerы, которые в Boost-e есть. Но видимо, изучать что-то стОит, даже если это не используешь! Спасибо за комменты и полезную информацию :).

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