• Как запрограммировать класс
vector<X>
, если тип
X
не имеет значения по умолчанию?
• Как гарантировать, что элементы вектора будут уничтожены в конце работы с ним?
Должны ли мы вообще решать эти проблемы? Мы могли бы заявить: “Не создавайте векторы для типов, не имеющих значений по умолчанию” или “Не используйте векторы для типов, деструкторы которых могут вызвать проблемы”. Для конструкции, предназначенной для общего использования, такие ограничения довольно обременительны и создают впечатление, что разработчик не понял задачи или не думал о пользователях. Довольно часто такие подозрения оказываются правильными, но разработчики стандартной библиотеки к этой категории не относятся. Для того чтобы повторить стандартный класс vector, мы должны устранить две указанные выше проблемы.
Мы можем работать с типами, не имеющими значений по умолчанию, предоставив пользователю возможность задавать это значение самостоятельно.
template<class T> void vector<T>::resize(int newsize, T def = T());
Иначе говоря, используйте в качестве значения по молчанию объект, созданный конструктором
T()
, если пользователь не указал иначе. Рассмотрим пример.
vector<double> v1;
v1.resize(100); // добавляем 100 копий объекта double(), т.е. 0.0
v1.resize(200, 0.0); // добавляем 200 копий числа 0.0 — упоминание
// излишне
v1.resize(300, 1.0); // добавляем 300 копий числа 1.0
struct No_default {
No_default(int); // единственный конструктор класса No_default
// ...
};
vector<No_default> v2(10); // ошибка: попытка создать 10
// No_default()
vector<No_default> v3;
v3.resize(100, No_default(2)); // добавляем 100 копий объектов
// No_default(2)
v3.resize(200); // ошибка: попытка создать 200
// No_default()
Проблему, связанную с деструктором, устранить труднее. По существу, мы оказались в действительно трудной ситуации: в структуре данных часть данных проинициализирована, а часть — нет. До сих пор мы старались избегать неинициализированных данных и ошибок, которые ими порождаются. Теперь, как разработчики класса
vector
, мы столкнулись с проблемой, которой раньше, как пользователи класса
vector
, не имели.
Во-первых, мы должны найти способ для получения неинициализированной памяти и манипулирования ею. К счастью, стандартная библиотека содержит класс
allocator
, распределяющий неинициализированную память. Слегка упрощенный вариант приведен ниже.
template<class T> class allocator {
public:
// ...
T* allocate(int n); // выделяет память для n объектов типа T
void deallocate(T* p, int n); // освобождает память, занятую n
// объектами типа T, начиная с адреса p
void construct(T* p, const T& v); // создает объект типа T
// со значением v по адресу p
void destroy(T* p); // уничтожает объект T по адресу p
};
Если вам нужна полная информация по этому вопросу, обратитесь к книге The C++ Programming Language или к стандарту языка С++ (см. описание заголовка <memory> ), а также к разделу B.1.1. Тем не менее в нашей программе демонстрируются четыре фундаментальных операции, позволяющих выполнять следующие действия:
• Выделение памяти, достаточной для хранения объекта типа
T
без инициализации.
• Создание объекта типа
T
в неинициализированной памяти.
• Уничтожение объекта типа
T
и возвращение памяти в неинициализированное состояние.
• Освобождение неинициализированной памяти, достаточной для хранения объекта типа
T
без инициализации.
Не удивительно, что класс
allocator
— то, что нужно для реализации функции
vector<T>::reserve()
. Начнем с того, что включим в класс
vector
параметр класса
allocator
.
template<class T, class A = allocator<T> > class vector {
A alloc; // используем объект класса allocator для работы
// с памятью, выделяемой для элементов
// ...
};
Кроме распределителя памяти, используемого вместо оператора
new
, остальная часть описания класса
vector
не отличается от прежнего. Как пользователи класса
vector
, мы можем игнорировать распределители памяти, пока сами не захотим, чтобы класс
vector
управлял памятью, выделенной для его элементов, нестандартным образом. Как разработчики класса
vector
и как студенты, пытающиеся понять фундаментальные проблемы и освоить основные технологии программирования, мы должны понимать, как вектор работает с неинициализированной памятью, и предоставить пользователям правильно сконструированные объекты. Единственный код, который следует изменить, — это функции-члены класса
vector
, непосредственно работающие с памятью, например функция
vector<T>::reserve()
.
template<class T, class A>
void vector<T,A>::reserve(int newalloc)