| Lesson 2 | Design of the Stack |
| Objective | Examine the design of a dynamically sized stack in C++. |
Designing a dynamically sized stack in C++23 leverages modern language features to create a clean, efficient, and exception-safe implementation. By using `std::vector
A dynamically sized stack is a LIFO container whose capacity can grow (or be selected) at runtime. The abstract data type (ADT) stays the same:
The design choice is how to store elements so the stack can grow safely and efficiently. In C++23, there are two common designs:
In modern C++, the most practical “design” is to reuse a standard container with well-defined complexity, allocator support, and exception-safety. There are two typical approaches:
std::stack is a container adaptor that exposes only stack operations. By default, it uses std::deque for storage,
but you can choose std::vector if you prefer contiguous memory.
#include <stack>
#include <vector>
std::stack<int> s1; // default underlying container: deque
std::stack<int, std::vector<int>> s2; // vector-backed stack
If you want direct control over capacity and performance, a vector-backed stack is straightforward:
#include <vector>
#include <stdexcept>
#include <cstddef>
template <typename T>
class VectorStack {
public:
VectorStack() = default;
explicit VectorStack(std::size_t reserveCount) { data_.reserve(reserveCount); }
void push(const T& v) { data_.push_back(v); }
void push(T&& v) { data_.push_back(std::move(v)); }
void pop() {
if (data_.empty()) throw std::runtime_error("Stack is empty");
data_.pop_back();
}
[[nodiscard]] T& top() {
if (data_.empty()) throw std::runtime_error("Stack is empty");
return data_.back();
}
[[nodiscard]] const T& top() const {
if (data_.empty()) throw std::runtime_error("Stack is empty");
return data_.back();
}
[[nodiscard]] bool empty() const noexcept { return data_.empty(); }
[[nodiscard]] std::size_t size() const noexcept { return data_.size(); }
private:
std::vector<T> data_;
};
This design is “dynamically sized” because std::vector grows automatically. It also follows the Rule of Zero:
no manual new/delete, so copying/moving/destruction are correct by default.
A linked-list stack grows one node at a time and avoids reallocations, but it trades away cache locality and increases per-element overhead.
If you implement it yourself in C++23, the key modernization is: use RAII ownership for nodes (typically std::unique_ptr),
and ensure non-throwing destruction.
Below is a C++23-friendly singly-linked design. It’s simpler than a doubly linked list for a stack (you only ever touch the top):
#include <memory>
#include <stdexcept>
#include <utility>
#include <cstddef>
template <typename T>
class DynamicStack {
private:
struct Node {
T data;
std::unique_ptr<Node> next;
explicit Node(T v, std::unique_ptr<Node> n = nullptr)
: data(std::move(v)), next(std::move(n)) {}
};
std::unique_ptr<Node> top_;
std::size_t size_ = 0;
public:
DynamicStack() = default;
// Rule of Zero is not available here because unique_ptr prevents implicit copying.
// For a teaching stack, we can support moves and optionally implement deep copy later.
DynamicStack(DynamicStack&&) noexcept = default;
DynamicStack& operator=(DynamicStack&&) noexcept = default;
void push(const T& v) {
top_ = std::make_unique<Node>(v, std::move(top_));
++size_;
}
void push(T&& v) {
top_ = std::make_unique<Node>(std::move(v), std::move(top_));
++size_;
}
void pop() {
if (!top_) throw std::runtime_error("Stack is empty");
top_ = std::move(top_->next);
--size_;
}
[[nodiscard]] T& top() {
if (!top_) throw std::runtime_error("Stack is empty");
return top_->data;
}
[[nodiscard]] const T& top() const {
if (!top_) throw std::runtime_error("Stack is empty");
return top_->data;
}
[[nodiscard]] bool empty() const noexcept { return top_ == nullptr; }
[[nodiscard]] std::size_t size() const noexcept { return size_; }
};
top_ is destroyed or reassigned.pop(), so it won’t throw.nullptr over NULL.[[nodiscard]] for query functions like empty() and size().noexcept when they truly cannot throw.top() to avoid unnecessary copies.#include <iostream>
int main() {
VectorStack<int> s;
s.push(10);
s.push(20);
std::cout << "Top: " << s.top() << "\n";
s.pop();
std::cout << "Size: " << s.size() << "\n";
}