Перейти к содержанию

Продвинутый проект: обобщённый контейнер

Начало урока

Продвинутый проект: обобщённый контейнер

Итоговый проект: пошагово строим шаблонный, поддерживающий перемещение Vec<T> с итераторами

Главная идея: Все абстракции, которые вы встретили в этом курсе — шаблоны, правило пяти, семантика перемещения, RAII, идеальная передача, безопасность исключений и итераторы — существуют, чтобы решить одну трудную задачу: контейнер, владеющий сырым блоком памяти и управляющий временем жизни объектов внутри него, для любого типа элемента и без единой лишней копии. В этом итоговом проекте мы проектируем и строим именно такой контейнер: растущий динамический массив, который назовём 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.

Продолжите этот урок на YourAIZone

В полном уроке — объяснение целиком, разобранные примеры, частые ошибки, тесты с мгновенной проверкой и ИИ-репетитор. Начните сразу как гость или создайте аккаунт, чтобы сохранять прогресс.

В полном уроке

  • Объяснение целиком
  • Разобранные примеры
  • Частые ошибки и как их избежать
  • Тестовые вопросы: 15
  • ИИ-репетитор, который отвечает на вопросы по теме
  • Отслеживание прогресса для ученика, родителей и учителя