Материалы книги получены с http://www.itlibitum.ru/
Управление памятью с применением ведущих указателей
Похоже, я соврал; в этой главе нам все же придется вернуться к умным указателям. Управление памятью под руководством клиента можно усовершенствовать, инкапсулируя различные стратегии в умных ведущих указателях. Расширение архитектуры с локальными пулами демонстрирует основную идею, которая может быть приспособлена практически для любой схемы с управлением на уровне объектов.
Специализированные ведущие указатели
Простейшая стратегия заключается в создании специализированного класса ведущего указателя или шаблона, который знает о локальном пуле и использует глобальную перегрузку оператора 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
}
Назад Содержание Далее
|