|
Управление памятью с применением ведущих указателей Похоже, я соврал; в этой главе нам все же придется вернуться к умным указателям. Управление памятью под руководством клиента можно усовершенствовать, инкапсулируя различные стратегии в умных ведущих указателях. Расширение архитектуры с локальными пулами демонстрирует основную идею, которая может быть приспособлена практически для любой схемы с управлением на уровне объектов. Специализированные ведущие указатели Простейшая стратегия заключается в создании специализированного класса ведущего указателя или шаблона, который знает о локальном пуле и использует глобальную перегрузку оператора new. struct Pool { ... }; // Как и раньше void* operator new(Pool* p); // Выделение из пула template <class Type> class PoolMP { private: Type* pointee; PoolMP(const PoolMP<Type>&) {} // Копирование не разрешено... PoolMP<Type>& operator=(const PoolMP<Type>&) { return *this; } // ...и присваивание тоже public: PoolMP(Pool* p) : pointee(new(p) Type) {} ~PoolMP() { pointee->~Type(); } Type* operator->() const { return pointee; } }; При желании клиент может использовать PoolMP для выделения и освобождения памяти в локальном пуле. Деструктор ведущего указателя вызывает деструктор указываемого объекта, но не освобождает память. Поскольку ведущий указатель не следит за исходным пулом, копирование и присваивание поддерживать не удастся, так как ведущий указатель понятия не имеет, в каком пуле создавать новые копии. Если не считать этих недостатков, перед нами фактически простейший указатель, не отягощенный никакими издержками. На это можно возразить, что копирование и присваивание все же следует поддерживать, но с использование операторов new и delete по умолчанию. В этом случае конструктор копий и оператор = работают так же, как и для обычного ведущего указателя. Обратные указатели на пул Чтобы поддерживать копирование и присваивание в пуле, можно запоминать адрес пула. template <class Type> class PoolMP { private: Type* pointee; Pool* pool; public: PoolMP(Pool* p) : pointee(new(p) Type), pool(p) {} ~PoolMP() { pointee->Type::~Type(); } PoolMP(const PoolMP<Type>& pmp) : pointee(new(pool) Type(*pointee)) {} PoolMP<Type>& operator=(const PoolMP<Type>& pmp) { if (this == &pmp) return *this; delete pointee; pointee = new(pool) Type(*pointee); return *this; } Type* operator->() const { return pointee; } }; Это обойдется вам в четыре лишних байта памяти, но не потребует лишних тактов процессора по сравнению с использованием обычных ведущих указателей. Сосуществование с обычными ведущими указателями Предложенное решение отнюдь не идеально. Интерфейс PoolMP открывает многое из того, о чем следовало бы знать только классам. Более того, если вам захочется совместно работать с объектами из пула и объектами, размещенными другим способом (например, с помощью стандартного механизма), начинаются настоящие трудности. Ценой добавления v-таблицы мы сможем значительно лучше инкапсулировать отличия в стратегиях управления памятью. template <class Type> class MP { protected: MP(const MP<Type>&) {} // Копирование не разрешено MP<Type>& operator=(const MP<Type>&) { return *this; } // Присваивание - тоже MP() {} // Используется только производными классами public: virtual ~MP() {} // Освобождение выполняется производными классами virtual Type* operator->() const = 0; }; template <class Type> class DefaultMP : public MP<Type> { private: Type* pointee; public: DefaultMP() : pointee(new Type) {} DefaultMP(const DefaultMP<Type>& dmp) : pointee(new Type(*dmp.pointee)) {} virtual ~DefaultMP() { delete pointee; } DefaultMP<Type>& operator=(const DefaultMP<Type>& dmp) { if (this == &dmp) return *this; delete pointee; pointee = new Type(*dmp.pointee); return *this; } virtual Type* operator->() const { return pointee; } }; template <class Type> class LocalPoolMP : public MP<Type> { private: Type* pointee; Pool* pool; public: LocalPoolMP(Pool* p) : pointee(new(p) Type), pool(p) [] LocalPoolMP(const LocalPoolMP<Type>& lpmp) : pointee(new(lpmp.pool) Type(*lpmp.pointee)), pool(lpmp.pool) {} virtual ~LocalPoolMP() { pointee->Type::~Type(); } LocalPoolMP<Type>& operator=(const LocalPoolMP<Type>& lpmp) { if (this == &lpmp) return *this; pointee->Type::~Type(); pointee = new(pool) Type(*lpmp.pointee); return *this; } virtual Type* operator->() const { return pointee; } }; Теперь DefaultMP и LocalPoolMP можно использовать совместно - достаточно сообщить клиенту, что они принадлежат к типу MP<Type>&. Копирование и присваивание поддерживается для тех классов, которые взаимодействуют с производными классами, но запрещено для тех, которые знают только о базовом классе. В приведенном коде есть одна тонкость: операторная функция LocalPoolMP::operator= всегда использует new(pool) вместо new(lpmp.pool). Это повышает безопасность в тех ситуациях, когда два ведущих указателя поступают из разных областей действия и разных пулов. Невидимые указатели Раз уж мы «заплатили вступительный взнос» и создали иерархию классов ведущих указателей, почему бы не пойти дальше и не сделать эти указатели невидимыми? Вместо применения шаблона нам придется реализовать отдельный класс указателя для каждого класса указываемого объекта, но это не слишком большая цена за получаемую гибкость. // В файле foo.h class Foo { public: static Foo* make(); // Использует выделение по умолчанию static Foo* make(Pool*); // Использует пул virtual ~Foo() {} // Далее следуют чисто виртуальные функции }; // В файле foo.cpp class PoolFoo : public Foo { private: Foo* foo; Pool* pool; public: PoolFoo(Foo* f, Pool* p) : foo(f), pool(p) {} virtual ~PoolFoo() { foo->~Foo(); } // Переопределения функций класса, делегирующие к foo }; class PFoo : public Foo { // Обычный невидимый указатель }; class ConcreteFoo : public Foo { ... }; Foo* Foo::make() { return new PFoo(new ConcreteFoo); } Foo* Foo::make(Pool* p) { return new PoolFoo(new(p) ConcreteFoo, p); } Такой вариант намного «чище» для клиента. Единственное место, в котором клиентский код должен знать что-то о пулах, - создание объекта функцией make(Pool*). Остальные пользователи полученного невидимого указателя понятия не имеют, находится их рабочий объект в пуле или нет. Стековые оболочки Чтобы добиться максимальной инкапсуляции, следует внести в описаннуюархитектуру следующие изменения: 1. Сделать Pool чисто абстрактным базовым классом с инкапсулированными производными классами, производящими функциями и т.д. 2. Предоставить функцию static Foo::makePool(). Функция make(Pool*) будет работать и для других разновидностей Pool, но makePool() позволяет Foo выбрать производящую функцию Pool, оптимальную для хранения Foo (например, с передачей размера экземпляра). 3. Переработать старый шаблон MP из предыдущих глав (с операторной функцией operator Type*()), чтобы при выходе из пула и указателей за пределы области действия все необходимое автоматически уничтожалось. Ниже показан примерный вид полученного интерфейса, с фрагментом клиентского кода и без виртуального оператора =. // В файле foo.h // Подключить объявление чисто абстрактного базового класса #include "pool.h" class Foo { private: Foo(const Foo&) {} Foo& operator=(const Foo&) { return *this; } public: static Pool* makePool(); // Создать пул, оптимизированный для Foo static Foo* make(); // Не использует пул static Foo* make(Pool*); // Использует пул // И т.д. }; // Клиентский код void g(Foo*); void f() { MP<Pool> pool(Foo::makePool()); MP<Foo> foo(Foo::make(pool)); foo->MemberOfFoo(); // Использует операторную функцию operator->() g(foo); // Использует операторную функцию operator Type*() // Выход из области действия - удаляется сначала foo, затем pool } |
Copyright 2005. Климов Александр. All Right Reserved.