Advanced Project: A Generic Container
The capstone: build a templated, move-aware Vec<T> with iterators, step by step
Vec<T>, essentially a teaching-grade std::vector. Nothing here is magic once you see how the pieces fit.
- 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
reservethat moves elements when it can do so safely, - add
push_backand a perfectly-forwardingemplace_back, - expose raw-pointer iterators so range-
forand 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.