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

Вступление


А начнем мы с начала: в общем случае умный указатель — это некоторая надстройка над обыкновенным указателем, которая добавляет в него полезную или требуемую функциональность. Значит это все то, что он должен предоставлять все те же возможности по работе с указателем (разыменование, получение доступа к полям или функциям из-под указателя) и заниматься «грязной работой» — предотвращать утечки, избегать обращения к ранее освобожденному участку памяти. Хотелось бы сразу сказать, что мне случалось видеть создание объектов умного указателя только для адресов, под которыми лежат объекты классов, что верно, ведь зачистки памяти под указателями подошел бы и обычный 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)


  1. DimaKurb
    14.11.2016 13:28
    +1

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


  1. Bonce
    14.11.2016 13:29
    +1

    При переопределении оператора "=" возврат значения будет происходить не во всех случаях. Наверное, следует добавить return *this; последней строкой.


    1. kartograph
      14.11.2016 13:34
      +1

      Да, верно. Спасибо!


  1. TheCalligrapher
    14.11.2016 13:29
    +1

    Не совсем понятно, зачем было "замусоривать" статью для начинающих (как вы сам сказали), некрасивым хаком с 'memcpy' в операторе присваивания? Неужели нельзя было выполнить обмен буквально?


    1. kartograph
      14.11.2016 13:40

      Хотелось сделать надёжнее, прием все таки полезный, но вы правы.


  1. skynowa
    14.11.2016 13:30
    +1

    Зачем count создавать динамически?


    1. kartograph
      14.11.2016 13:33

      Отвечу сразу всем: счетчик создаю динамически, потому что создав оный на стеке второй вариант main не сработает, ресурс первого указателя будет возвращен и образуется висячий указатель sh1, появление которого мы хотели избежать.


  1. kachsheev
    14.11.2016 13:30

    int* count
    

    Зачем? Ведь можно было создавать счётчик на стеке.

    resource(NULL)
    

    А как же nullptr?


    1. bfDeveloper
      14.11.2016 13:49

      На стеке нельзя, счётчик должен быть общий между всеми указателями на один и тот же объект


  1. bfDeveloper
    14.11.2016 13:54
    +2

    Как отметили выше, для новичков статья не очень. Рассказали бы лучше зачем это надо, где использовать, а где не надо. Про существующие shared_ptr с его thread safe; про unique_ptr, который гораздо более предпочтителен, про weak_ptr и второй счётчик ссылок в shared_ptr, чтобы вовремя удалять count.
    А новичкам, да и вообще всем рекомендую Scott Meyers: Modern Effective C++ . Там есть глава про умные указатели со всеми подробностями. Да и все остальные главы отличные.


    1. kartograph
      14.11.2016 17:31

      Даже на Хабре есть достаточно хорошая статья, которую я бы однозначно рекомендовал. Единственное чего там не было — вышеописанного счетчика.


  1. Chaos_Optima
    14.11.2016 16:18

    Ужас, мало того что код написан с ошибками

            T* resource;
            int* count  // ; где?
    public:
    ....
    shr_ptr(const shr_ptr<T>& R): // : - ?
    

    Может стоило хотя бы скомпилить код?
    Ну и да утечка памяти в статье про умные указатели, шикарно.
    Оператор копирования дырявый
    прув: https://ideone.com/ajkyDX
    Зачем такие статьи, непонятно. Минус.


    1. kartograph
      14.11.2016 16:38
      -1

      Все исправлено, ликов нет, можете проверить.


      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 использовать? это крайне странно


        1. kartograph
          14.11.2016 17:12

          Здесь уже вы не правы, в вашем примере все три указателя в итоге будут ссылаться на один ресурс, то есть у первого ресурса счетчик 1, значит он удалится сразу после sh1=sh2, а дальше получится у sh1 счетчик равен 3, у sh2 — 1, у sh3 — 2, стало быть, удаление пройдет нормально и гарантировано только один раз. Попробуйте определить область жизни ваших умных указателей, чтобы деструктор каждого сработал до return.


          1. Chaos_Optima
            14.11.2016 17:15

            да простите, мемкопи неправильно просмотрел, но всё равно это не отменяет того как ужасно написан код


            1. kartograph
              14.11.2016 17:22
              -1

              Насчет memcpy — это довольно удобный способ расписать оператор присваиванию, впрочем, буду рад, если предложите что-небудь более оптимальное или скинете ссылку на ресурс с информацией, тогда я тотчас же изменю реализацию. А какие у вас еще претензии к коду?


              1. 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 а не мусор какой нибудь, в следствии чего можно легко словить сегфолт, или в многопоточной среде, вообще ужас программиста, плавующий баг.


                1. kartograph
                  14.11.2016 17:45

                  Если вам приятнее видеть реализацию оператора = в таком виде, значит изменю, хоть теперешняя реализация по-моему работает точно так же. Насчет второго и третьего замечаний: вы абсолютно правы, спасибо за фидбэк.


                  1. Chaos_Optima
                    14.11.2016 18:03

                    Работает то также, но дольше и выглядит как хак, собственно говоря им и являясь.


                  1. PkXwmpgN
                    14.11.2016 18:10

                    В качестве рекомендации, по поводу оператора присваивания.
                    Есть такое понятие как каноническая реализация, в основе которого лежит идиома copy-and-swap. У вас правильная идея с временным объектом, просто явная реализация через memcpy сильно "бросается в глаза".


                1. TheCalligrapher
                  14.11.2016 19:22
                  -3

                  Ваше предложение о том, как "должен" быть написан оператор присваивания — мимо кассы.


                  Автор статьи реализовал хорошо известную идиому copy-and-swap (он реализовал ее через memcpy, что не совсем красиво, но тем не менее). Это — правильно. Да, присваивание надо делать через copy-and-swap.


                  Поэтому не надо нам тут рассказывать про "должен был выглядеть как минимум так". Нет, не должен был.


                  1. Chaos_Optima
                    15.11.2016 12:09

                    cas хорошая вещь, но тут она вообще не несёт какого либо дополнительного смысла, и добавляет лишних операций. К тому же, для обучающей статьи это явно лишнее. Ну или хотя бы написал что это cas.


                    1. TheCalligrapher
                      15.11.2016 20:37

                      Удалил — бесполезно.


              1. 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)
                


                1. kartograph
                  14.11.2016 17:54

                  Да, я это понял, ошибка действительно явная, буду с ней бороться. Не получиться — спрячу статью в черновики