Skip to content

Advanced Project: A Generic Container

Beginning of the lesson

Advanced Project: A Generic Container

The capstone: build a templated, move-aware Vec<T> with iterators, step by step

Big Idea: Every abstraction you met in this course — templates, the rule of five, move semantics, RAII, perfect forwarding, exception safety and iterators — exists to solve one hard problem: a container that owns a raw block of memory and manages the lifetime of the objects inside it, for any element type, with zero avoidable copies. In this capstone we design and build exactly that: a growable dynamic array we will call Vec<T>, essentially a teaching-grade std::vector. Nothing here is magic once you see how the pieces fit.
By the end you will be able to:
  • separate allocation from construction using raw storage and placement new,
  • implement the full rule of five so the container copies, moves and cleans up correctly,
  • write a geometric-growth reserve that moves elements when it can do so safely,
  • add push_back and a perfectly-forwarding emplace_back,
  • expose raw-pointer iterators so range-for and the STL algorithms just work.

1. The interface we are targeting

Before writing any body, pin down the public shape. A useful sequence container needs three numbers and a handful of operations. We keep three data members: a pointer to the storage, the number of constructed elements (size), and the number of element slots the storage can hold (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_; }
};

The single most important idea is the split between size and capacity. Capacity is how much memory we hold; size is how many live objects occupy the front of that memory. The gap between them is raw, uninitialised bytes — no T lives there yet.

Continue this lesson on YourAIZone

The full lesson has the complete explanation, worked examples, common mistakes, tests with instant checking and an AI tutor. Start right away as a guest, or create an account to keep your progress.

In the full lesson

  • The complete explanation
  • Worked examples
  • Common mistakes and how to avoid them
  • Test questions: 15
  • AI tutor that answers questions about the topic
  • Progress tracking for students, parents and teachers