Шаг 190.
Библиотека STL.
Контейнеры STL. Реализация ссылочной семантики

    На этом шаге мы рассмотрим реализацию ссылочной семантики.

    Контейнерные классы STL поддерживают семантику значений, но не поддерживают ссылочную семантику. Они создают внутренние копии вставляемых элементов и затем возвращают эти копии. На 119 шаге рассматриваются достоинства и недостатки обеих семантик, а также некоторые последствия, к которым приводит выбор. Напоминаем один из выводов: если вам потребуется ссылочная семантика в контейнерах STL (например, из-за того, что копирование элементов обходится слишком дорого или же элементы должны совместно использоваться несколькими коллекциями), воспользуйтесь классом умного указателя для предотвращения потенциальных ошибок. Ниже приведено одно из возможных решений проблемы. В нем задействован вспомогательный класс умного указателя с подсчетом ссылок на объекты, на которые ссылается указатель:

#ifndef COUNTED_PTR_HPP 
#define COUNTED_PTR_HPP
// Класс, обеспечивающий семантику подсчета ссылок
// - объект, на который ссылается указатель, автоматически
// уничтожается при удалении последнего экземпляра CountedPtr
// для данного объекта.

template <class T> 
class CountedPtr { 
  private:
    T* ptr;         // Указатель на значение
    long* count;    // Количество владельцев (общие данные)
  public:
    // Инициализация объекта существующим указателем 
    // - указатель р должен быть лолучен в результате вызова new 
    explicit CountedPtr (T* p=0) 
      : ptr(p), count(new long(1)) {}

    // Копирующий указатель (увеличивает счетчик владельцев) 
    CountedPtr (const CountedPtr<T>& p) throw() 
       : ptr(p.ptr), count(p.count) { 
      ++*count;
    }

    // Деструктор (уничтожает объект, если владелец был последним) 
    ~CountedPtr () throw() { 
      dispose();
    }

    // Присваивание (перевод указателя на новый объект) 
    CountedPtr<T>& operator= (const CountedPtr<T>& p) throw() { 
      if (this != &p) {
        dispose();
        ptr = p.ptr;
        count = p.count;
        ++*count;
      }
      return *this;
    }

    // Доступ к объекту, на который ссылается указатель 
    T& operator*() const throw() { 
      return *ptr;
    }
    T* operator->() const throw() { 
      return ptr;
    }

  private: 
    void dispose() {
      if (--*count == 0) { 
        delete count; 
        delete ptr;
      }
    }
};
#endif //COUNTED_PTR_HPP

    Класс напоминает стандартный класс умного указателя auto_ptr (смотри 59 шаг). Предполагается, что значения, которыми инициализируются умные указатели, были возвращены оператором new. В отличие от класса auto_ptr это позволяет копировать умные указатели так, чтобы оригинал и копия оставались действительными. Объект уничтожается только после удаления последнего указателя, ссылающегося на него.

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

    Ниже приведен пример использования класса CountedPtr:

//---------------------------------------------------------------------------

#include <vcl.h>
#include <iostream>
#include <list>
#include <deque>
#include <algorithm>

#include "countptr.hpp"
#include <conio.h> //необходимо для getch()

#pragma hdrstop

//---------------------------------------------------------------------------

#pragma argsused
using namespace std;


std::string ToRus(const std::string &in)
{
  char *buff = new char [in.length()+1];
  CharToOem(in.c_str(),buff);
  std::string out(buff);
  delete [] buff;
  return out;
}

void printCountedPtr (CountedPtr<int> elem)
{
  cout << *elem << ' ';
}

int main(int argc, char* argv[])
{
  // Массив целых чисел (для совместного использования
  // в разных контейнерах)
  static int values[] = { 3, 5, 9, 1, 6, 4 };

  // Две разные коллекции 
  typedef CountedPtr<int> IntPtr; 
  deque<IntPtr> coll1; 
  list<IntPtr> coll2;

  // Вставка общих объектов в коллекции
  // - исходный порядок в coll1
  // - обратный лорядок в coll2

  for (int i=0; i<sizeof(values)/sizeof(values[0]); ++i) { 
    IntPtr ptr(new int(values[i])); 
    coll1.push_back(ptr); 
    coll2.push_front(ptr);
  }

  // Вывод содержимого обеих коллекций 
  cout << ToRus("Коллекция coll1:\n");
  for_each (coll1.begin(),coll1.end(),  
            printCountedPtr); 
  cout << endl;

  cout << ToRus("Коллекция coll2:\n");
  for_each (coll2.begin(), coll2.end(),
            printCountedPtr); 
  cout << endl << endl;

 // Модификация значений в разных коллекциях
 // - возведение в квадрат третьего значения в coll1
 // - изменение знака первого значения в coll1
 // - обнуление первого значения в coll2

 *coll1[2] *= *coll1[2]; 
  (**coll1.begin()) *= -1; 
  (**coll2.begin()) = 0;

  // Повторный вывод содержимого обеих коллекций 
  cout << ToRus("Коллекция coll1 после изменения:\n");
  for_each (coll1.begin(),coll1.end(),  
            printCountedPtr); 
  cout << endl;

  cout << ToRus("Коллекция coll2 после изменения:\n");
  for_each (coll2.begin(), coll2.end(),
            printCountedPtr); 
  cout << endl << endl;

  getch();
  return 0;
}

//---------------------------------------------------------------------------
Текст этого примера можно взять здесь.

    Результат выполнения программы выглядит так:


Рис.1. Результат работы приложения

    Если вызвать вспомогательную функцию, сохраняющую элемент коллекции (IntPtr) где-то в другом месте, то значение, на которое ссылается указатель, остается действительным даже после уничтожения коллекций или удаления из них всех элементов.

    В архиве Boost библиотек C++ (http://www.boost.org/) хранятся различные классы умных указателей, расширяющие стандартную библиотеку C++ (вероятно, вместо CountedPtr<> стоит поискать название shared_ptr<>).

    На следующем шаге мы приведем рекомендации по выбору контейнера.




Предыдущий шаг Содержание Следующий шаг