| Lesson 3 | Stack constructors |
| Objective | Take a closer look at the ch_stack constructors. |
C++ stacks provide several constructors that allow flexible initialization depending on your needs. The default constructor `stack
new[] + assert encourages fragile ownership patterns; modern C++ prefers RAII containers (std::vector, std::string) and the Rule of Zero.ch_stack d[N] are fine for compile-time N, but real code typically uses std::array or std::vector for clarity and safety.
In the previous lesson, ch_stack was designed so the client can choose a stack capacity at construction time.
In C++, that “choice” is expressed through constructor overloads: same class name, different parameter lists.
The overload the compiler selects depends only on the arguments you pass at the call site.
If a client knows the maximum number of elements up front, a “capacity” constructor makes the intent explicit:
ch_stack data(1000); // capacity: 1000 elements
ch_stack more_data(2 * n); // capacity: 2*n elements
"ABCD").
For learning purposes, building ch_stack is great. For production code, prefer std::stack<T> (container adaptor)
or std::vector<T> when you want direct access to the underlying storage.
Still, if you’re implementing ch_stack, modern C++ makes constructor design safer and simpler:
std::vector<char> (or std::string) instead of new[].top_ (0 means empty).std::string_view: accept string-like inputs without forcing a particular owning type.The following example shows a modern constructor set (default capacity, explicit capacity, and capacity + initial contents). Notice how the storage is managed automatically, and the constructors express intent through initialization lists.
#include <algorithm>
#include <cstddef>
#include <string_view>
#include <vector>
class ch_stack {
public:
// 1) Default constructor (conventional default capacity)
ch_stack() : ch_stack(100) {}
// 2) Capacity constructor
explicit ch_stack(std::size_t capacity)
: max_len_(capacity ? capacity : 100),
s_(max_len_),
top_(0) {}
// 3) Capacity + initial contents (seed the stack)
ch_stack(std::size_t capacity, std::string_view init)
: ch_stack(capacity)
{
const std::size_t n = std::min(init.size(), max_len_);
std::copy_n(init.begin(), n, s_.begin());
top_ = n; // current size (top index would be top_-1)
}
// (Push/pop omitted here; constructors are the focus.)
private:
std::size_t max_len_;
std::vector<char> s_;
std::size_t top_; // number of valid elements currently in the stack
};
Key idea: max_len_ is capacity, and top_ is the current size.
When top_ == 0, the stack is empty. When top_ == max_len_, the stack is full.
Now let’s map calling statements to specific constructors. These examples are intentionally short, because the selection rules are purely about argument lists.
ch_stack data; // calls ch_stack() ➜ delegates to ch_stack(100)
ch_stack w(4, "ABCD"); // calls ch_stack(std::size_t, std::string_view)
In legacy code you may see:
ch_stack d[N];
That creates N stack objects and calls the default constructor for each element.
The important detail: [N] is an array declarator, not a function argument, so it does not affect overload resolution.
In modern C++, you’ll usually prefer one of these forms:
#include <array>
#include <vector>
std::array<ch_stack, N> fixed; // when N is compile-time constant
std::vector<ch_stack> many(N); // when N is runtime or just more flexible
A class template lets you reuse the same stack logic for different element types. The “constructor story” stays the same—overloads choose capacity, initialization, and behavior—but the element type becomes a parameter.
Below is a modern template version that demonstrates the same constructor theme while avoiding raw pointers and custom destructors. This is closer to “current API” C++: containers handle memory, and copy/move operations work automatically (Rule of Zero).
#ifndef STACK_H
#define STACK_H
#include <cstddef>
#include <vector>
template <typename T>
class Stack {
public:
// default capacity is 10; validate capacity inline
explicit Stack(std::size_t capacity = 10)
: size_(capacity ? capacity : 10),
top_(0),
data_(size_) {}
bool push(const T& value) {
if (isFull()) return false;
data_[top_++] = value;
return true;
}
bool pop(T& out) {
if (isEmpty()) return false;
out = data_[--top_];
return true;
}
bool isEmpty() const noexcept { return top_ == 0; }
bool isFull() const noexcept { return top_ == size_; }
private:
std::size_t size_; // capacity
std::size_t top_; // current size
std::vector<T> data_;
};
#endif
Note a practical constraint: constructing std::vector<T> data_(size_) default-constructs T elements,
so T must be default-constructible for this exact implementation. If that’s not acceptable, you’d switch to a design that
uses reserve + push_back (or stores std::optional<T> slots) depending on your requirements.
T.