| Lesson 10 | The ADT's interface |
| Objective | Understand what belongs in an ADT's interface when using C++23 |
Your lesson is fundamentally about interface design: what you expose to client code, what you keep hidden, and how you make the public API stable even if the implementation changes. In modern C++ terms, an “ADT” maps cleanly to a type contract or public API: a set of operations and guarantees (semantics, invariants, complexity, and error behavior) that clients can rely on.
Black Box thinking still applies in C++23:
clients should depend on what the type does, not how it does it.
That separation is what lets you replace an internal array with a std::vector,
add bounds checks, change storage strategy, or improve performance—without breaking callers.
A strong interface is more than “public functions.” In C++23, a well-designed type interface typically includes:
push, pop, peek/top, empty, size, clear).
Avoid exposing raw internals like top or the backing container.
pop returns the most recently pushed element that has not been removed.”
false, or return an std::optional?
Modern C++ often prefers explicit error signaling rather than “undefined behavior surprises.”
std::vector growth).
const
(e.g., empty(), size(), peek()).
Your original example makes a key point: clients should not reach inside the type and set top directly.
In C++23 we go further: we design the type so “reaching inside” isn’t possible at all.
That means:
std::vector) so memory management stays correct automatically.try_pop instead of “hope it works”).This is why “ADT” is not actually obsolete—it’s the timeless design idea. What is obsolete is implementing an abstraction in a way that forces clients to manage your invariants or memory. C++23 encourages interfaces that are safe by construction.
Below is a modern, self-contained example of a character stack with a clear interface. Notice what the interface includes:
push, try_pop, peek (core operations)empty, size, clear (useful observability and lifecycle)capacity and full only if the stack is intentionally boundedtry_pop returns false on empty instead of failing mysteriously#include <iostream>
#include <string>
#include <string_view>
#include <vector>
class CharStack {
public:
explicit CharStack(std::size_t capacity)
: cap_(capacity) {
data_.reserve(capacity); // avoids repeated reallocation
}
[[nodiscard]] bool empty() const noexcept { return data_.empty(); }
[[nodiscard]] bool full() const noexcept { return data_.size() >= cap_; }
[[nodiscard]] std::size_t size() const noexcept { return data_.size(); }
[[nodiscard]] std::size_t capacity() const noexcept { return cap_; }
void clear() noexcept { data_.clear(); }
// Core ADT operation: push (fails if bounded and full)
[[nodiscard]] bool push(char c) {
if (full()) return false;
data_.push_back(c);
return true;
}
// Safe pop: returns false if empty, otherwise writes the popped value.
[[nodiscard]] bool try_pop(char& out) noexcept {
if (data_.empty()) return false;
out = data_.back();
data_.pop_back();
return true;
}
// Non-mutating observation. Returns false if empty.
[[nodiscard]] bool peek(char& out) const noexcept {
if (data_.empty()) return false;
out = data_.back();
return true;
}
private:
std::size_t cap_;
std::vector<char> data_;
};
int main() {
constexpr std::size_t max_len = 64;
CharStack s(max_len);
std::string str = "My name is Don Knuth!";
std::cout << str << '\n';
for (char c : str) {
if (!s.push(c)) break; // stop if full
}
std::string reversed;
reversed.reserve(str.size());
char ch{};
while (s.try_pop(ch)) {
reversed.push_back(ch);
}
std::cout << reversed << '\n';
return 0;
}
This design is faithful to the ADT concept while using modern C++ idioms:
const and noexcept,
which makes intent clear and enables optimization.
new/delete, no leak risk, and exceptions (if you later add them)
won’t strand resources.
To “think like C++23” when building interfaces, keep these adjacent concepts in mind:
An ADT interface in C++23 is the combination of: operations + semantics + invariants + error behavior + complexity expectations. If clients can do everything they need through that interface—and cannot violate invariants— you’ve built a strong abstraction that can evolve internally without breaking external code.