Вступление
А начнем мы с начала: в общем случае умный указатель — это некоторая надстройка над обыкновенным указателем, которая добавляет в него полезную или требуемую функциональность. Значит это все то, что он должен предоставлять все те же возможности по работе с указателем (разыменование, получение доступа к полям или функциям из-под указателя) и заниматься «грязной работой» — предотвращать утечки, избегать обращения к ранее освобожденному участку памяти. Хотелось бы сразу сказать, что мне случалось видеть создание объектов умного указателя только для адресов, под которыми лежат объекты классов, что верно, ведь зачистки памяти под указателями подошел бы и обычный MemoryGuard, которому бы хватило одного delete.
Ближе к делу
Итак, перейдем непосредственно к реализации Shared Pointer. Сперва вести учет ресурсов не будем. Требования к классу будут следующие:
— Наличие конструктора, принимающего указатель на ресурс.
— Перегруженные операторы -> и *.
— Акссесор, позволяющий получить адрес ресурса.
Поскольку класс шаблонный, описание и реализацию можно выполнить в одному hpp-файле. Операторы -> и * могут быть определенные как члены класса, ведь слева от них будет всегда находится объект нашего умного указателя.
template<class T>
class shr_ptr
{
T* resource;
public:
shr_ptr(T* res=NULL):resource(res)
{
}
~shr_ptr()
{
delete resource;
}
T* operator ->() const
{
return resource;
}
T& operator * () const
{
return *resource;
}
T* get () const
{
return resource;
}
};
Такая реализация уже позволит нам безболезненно создавать указатели на объекты других классов и быть уверенными в том, что утечек не будет. Кстати, проверка утечек осуществляется с помощью функции _CrtDumpMemoryLeaks. Нижеуказанный фрагмент кода отлично отработает.
int main()
{
{
shr_ptr <SomeClass> sh (new SomeClass);
sh->do_operation();
}
if(_CrtDumpMemoryLeaks())
cout<<"LEAKS";
}
У такой реализации есть явный недостаток — стоит нам только создать несколько объектов умного указателя с помощью конструктора копирования, как тотчас же мы столкнемся с проблемой обращения к уже освобожденному куску памяти. Для решения оной можно воспользоваться счётчиком, который будет вести учет одинаковых ресурсов. Счетчик будет создаваться при срабатывании конструктора с параметром и увеличивается в конструкторе копирования и операторе присваивания.
template<class T>
class shr_ptr
{
T* resource;
int* count ;
public:
shr_ptr():resource(NULL),count(NULL)
{
}
shr_ptr(T* res):resource(res)
{
count = new int(1);
}
shr_ptr(const shr_ptr<T>& R)
{
if(R.resource!=NULL)
{
resource = R.resource;
count = R.count;
*count = *R.count+1;
}
}
~shr_ptr()
{
if(resource!=NULL && --*(count)==0)
{
delete resource;
delete count;
resource = NULL;
}
}
shr_ptr<T>& operator = (const shr_ptr<T>& R)
{
if(R.resource != this->resource)
{
shr_ptr<T> tmp (R);
char sBuff[sizeof(shr_ptr<T>)];
memcpy(sBuff,this,sizeof(shr_ptr<T>));
memcpy(this,&tmp,sizeof(shr_ptr<T>));
memcpy(&tmp,sBuff,sizeof(shr_ptr<T>));
}
return *this;
}
T* operator ->() const
{
return resource;
}
T& operator * () const
{
return *resource;
}
T* get () const
{
return resource;
}
};
Как вы можете видеть, счетчик создается для каждого нового ресурса, увеличивается при повторном создании и уменьшается при вызове деструктора.
После этого создав в main несколько объектов умного указателя с одинаковыми ресурсами мы гарантировано очистим память только один раз, то есть программа успешно отработает. Все благодаря нехитрому счетчику. Пример:
int main()
{
{
shr_ptr <SomeClass> sh (new SomeClass);
shr_ptr <SomeClass> sh1(sh);
shr_ptr <SomeClass> sh2 (new SomeClass);
sh = sh2;
sh->do_operation();
sh1->do_operation();
sh2->do_operation();
}
if(_CrtDumpMemoryLeaks())
cout<<"LEAKS";
}
Спасибо за внимание!
Комментарии (26)
Bonce
14.11.2016 13:29+1При переопределении оператора "=" возврат значения будет происходить не во всех случаях. Наверное, следует добавить return *this; последней строкой.
TheCalligrapher
14.11.2016 13:29+1Не совсем понятно, зачем было "замусоривать" статью для начинающих (как вы сам сказали), некрасивым хаком с 'memcpy' в операторе присваивания? Неужели нельзя было выполнить обмен буквально?
skynowa
14.11.2016 13:30+1Зачем count создавать динамически?
kartograph
14.11.2016 13:33Отвечу сразу всем: счетчик создаю динамически, потому что создав оный на стеке второй вариант main не сработает, ресурс первого указателя будет возвращен и образуется висячий указатель sh1, появление которого мы хотели избежать.
kachsheev
14.11.2016 13:30int* count
Зачем? Ведь можно было создавать счётчик на стеке.
resource(NULL)
А как же nullptr?bfDeveloper
14.11.2016 13:49На стеке нельзя, счётчик должен быть общий между всеми указателями на один и тот же объект
bfDeveloper
14.11.2016 13:54+2Как отметили выше, для новичков статья не очень. Рассказали бы лучше зачем это надо, где использовать, а где не надо. Про существующие shared_ptr с его thread safe; про unique_ptr, который гораздо более предпочтителен, про weak_ptr и второй счётчик ссылок в shared_ptr, чтобы вовремя удалять count.
А новичкам, да и вообще всем рекомендую Scott Meyers: Modern Effective C++ . Там есть глава про умные указатели со всеми подробностями. Да и все остальные главы отличные.kartograph
14.11.2016 17:31Даже на Хабре есть достаточно хорошая статья, которую я бы однозначно рекомендовал. Единственное чего там не было — вышеописанного счетчика.
Chaos_Optima
14.11.2016 16:18Ужас, мало того что код написан с ошибками
T* resource; int* count // ; где? public: .... shr_ptr(const shr_ptr<T>& R): // : - ?
Может стоило хотя бы скомпилить код?
Ну и да утечка памяти в статье про умные указатели, шикарно.
Оператор копирования дырявый
прув: https://ideone.com/ajkyDX
Зачем такие статьи, непонятно. Минус.kartograph
14.11.2016 16:38-1Все исправлено, ликов нет, можете проверить.
Chaos_Optima
14.11.2016 16:59+1Неужели так трудно пройтись компилятором перед тем как выкладывать?
char sBuff[sizeof(SharedPointer<T>)]; // SharedPointer откуда взялся?
Ну и да утечка по прежнему есть
shr_ptr<A> sh1 = new A; shr_ptr<A> sh2 = new A; shr_ptr<A> sh3 = sh2; sh1 = sh2;
выдаст
created
created
deleted
И да зачем memcpy использовать? это крайне странноkartograph
14.11.2016 17:12Здесь уже вы не правы, в вашем примере все три указателя в итоге будут ссылаться на один ресурс, то есть у первого ресурса счетчик 1, значит он удалится сразу после sh1=sh2, а дальше получится у sh1 счетчик равен 3, у sh2 — 1, у sh3 — 2, стало быть, удаление пройдет нормально и гарантировано только один раз. Попробуйте определить область жизни ваших умных указателей, чтобы деструктор каждого сработал до return.
Chaos_Optima
14.11.2016 17:15да простите, мемкопи неправильно просмотрел, но всё равно это не отменяет того как ужасно написан код
kartograph
14.11.2016 17:22-1Насчет memcpy — это довольно удобный способ расписать оператор присваиванию, впрочем, буду рад, если предложите что-небудь более оптимальное или скинете ссылку на ресурс с информацией, тогда я тотчас же изменю реализацию. А какие у вас еще претензии к коду?
Chaos_Optima
14.11.2016 17:31Код присваивания должен был выглядеть как минимум так
shr_ptr<T>& operator = (const shr_ptr<T>& R) { if (R.resource != resource) { *counter -= 1; if (!counter) { delete resource; delete count; } resource = R.resource; count = R.count; *count += 1; } return *this; }
Во вторых у вас попрежнему есть утечка
shr_ptr sh1 = NULL;
при выходе из скоупа у вас утечёт int* count
В третьих
shr_ptr(const shr_ptr<T>& R) { if (R.resource != NULL) { resource = R.resource; count = R.count; *count = *R.count + 1; } }
если R.resource будет равен нулю то ваши поля будут не инициализированны, при тестах вам везло что они инициализировались 0 но в компилятор не гарантирует что там будут 0 а не мусор какой нибудь, в следствии чего можно легко словить сегфолт, или в многопоточной среде, вообще ужас программиста, плавующий баг.kartograph
14.11.2016 17:45Если вам приятнее видеть реализацию оператора = в таком виде, значит изменю, хоть теперешняя реализация по-моему работает точно так же. Насчет второго и третьего замечаний: вы абсолютно правы, спасибо за фидбэк.
Chaos_Optima
14.11.2016 18:03Работает то также, но дольше и выглядит как хак, собственно говоря им и являясь.
PkXwmpgN
14.11.2016 18:10В качестве рекомендации, по поводу оператора присваивания.
Есть такое понятие как каноническая реализация, в основе которого лежит идиома copy-and-swap. У вас правильная идея с временным объектом, просто явная реализация через memcpy сильно "бросается в глаза".
TheCalligrapher
14.11.2016 19:22-3Ваше предложение о том, как "должен" быть написан оператор присваивания — мимо кассы.
Автор статьи реализовал хорошо известную идиому copy-and-swap (он реализовал ее через
memcpy
, что не совсем красиво, но тем не менее). Это — правильно. Да, присваивание надо делать через copy-and-swap.
Поэтому не надо нам тут рассказывать про "должен был выглядеть как минимум так". Нет, не должен был.
Chaos_Optima
15.11.2016 12:09cas хорошая вещь, но тут она вообще не несёт какого либо дополнительного смысла, и добавляет лишних операций. К тому же, для обучающей статьи это явно лишнее. Ну или хотя бы написал что это cas.
Chaos_Optima
14.11.2016 17:43Пример для третьего случая
int main() { char* test_heap = new char[sizeof(shr_ptr<A>)]; for (int i = 0; i < sizeof(shr_ptr<A>); i++) test_heap[i] = 1; { shr_ptr<A> sh1 = NULL; shr_ptr<A>* sh2 = new(test_heap) shr_ptr<A>(sh1); sh1 = *sh2; delete sh3; } return 0; }
p.s. не успел отредактировать предыдущий комментарий
*count-= 1; if (!count) //вместо *counter -= 1; if (!counter)
kartograph
14.11.2016 17:54Да, я это понял, ошибка действительно явная, буду с ней бороться. Не получиться — спрячу статью в черновики
DimaKurb
как по мне, для начинающих лучше пояснить в чем идея умного указателя и когда его использовать. также пояснить почему и когда умный указатель очистит память тем самым избежав утечки, ИМХО.