Продвинутый проект: обобщённый контейнер
Итоговый проект: пошагово строим шаблонный, поддерживающий перемещение Vec<T> с итераторами
Vec<T> — по сути учебный std::vector. Здесь нет никакой магии, стоит лишь увидеть, как складываются детали.
- отделять выделение памяти от конструирования с помощью сырого хранилища и placement new,
- реализовать полное правило пяти, чтобы контейнер правильно копировался, перемещался и очищался,
- написать
reserveс геометрическим ростом, который перемещает элементы, когда это безопасно, - добавить
push_backи идеально передающийemplace_back, - предоставить итераторы-сырые-указатели, чтобы range-
forи алгоритмы STL заработали сами собой.
1. Интерфейс, к которому мы стремимся
Прежде чем писать тело, зафиксируем публичную форму. Полезному последовательному контейнеру нужны три числа и горстка операций. Мы храним три поля: указатель на хранилище, число сконструированных элементов (size) и число слотов, которые хранилище может вместить (capacity).
template <typename T>
class Vec {
T* data_ = nullptr; // raw block of capacity_ slots
std::size_t size_ = 0; // how many are actually constructed
std::size_t cap_ = 0; // how many slots exist
public:
Vec() = default;
~Vec(); // destroy + free
Vec(const Vec&); // copy
Vec& operator=(const Vec&);
Vec(Vec&&) noexcept; // move
Vec& operator=(Vec&&) noexcept;
void reserve(std::size_t n); // grow storage
void push_back(const T& v);
void push_back(T&& v);
template <typename... Args>
T& emplace_back(Args&&... args); // construct in place
std::size_t size() const { return size_; }
std::size_t capacity() const { return cap_; }
T& operator[](std::size_t i) { return data_[i]; }
T* begin() { return data_; } // iterators = raw pointers
T* end() { return data_ + size_; }
};
Самая важная мысль — разделение size и capacity. Capacity — сколько памяти мы удерживаем; size — сколько живых объектов занимают начало этой памяти. Промежуток между ними — сырые, неинициализированные байты: там ещё не живёт ни один T.