На этом шаге мы рассмотрим реализацию ссылочной семантики.
Контейнерные классы 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<>).
На следующем шаге мы приведем рекомендации по выбору контейнера.