OO Encapsulation  «Prev  Next»
Lesson 10 The ADT's interface
Objective Understand what belongs in an ADT's interface when using C++23

Designing an ADT Interface in 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.

What Belongs in an ADT’s Interface

A strong interface is more than “public functions.” In C++23, a well-designed type interface typically includes:

  1. Public operations that represent meaningful actions on the abstraction (e.g., push, pop, peek/top, empty, size, clear). Avoid exposing raw internals like top or the backing container.
  2. Semantic guarantees (the real “ADT” part): what each operation means. Example: “pop returns the most recently pushed element that has not been removed.”
  3. Invariants that always hold for a valid object. For a stack: “size is never negative,” “size never exceeds capacity (if bounded),” and “the top element is the last pushed element.”
  4. Error behavior: what happens on misuse or boundary conditions. For example: popping an empty stack—does it throw, assert, return false, or return an std::optional? Modern C++ often prefers explicit error signaling rather than “undefined behavior surprises.”
  5. Complexity notes when they matter. Clients may assume stack operations are O(1) on average, but you should be honest about amortized behavior if you use dynamic storage (e.g., std::vector growth).
  6. Const-correctness: operations that do not mutate observable state should be const (e.g., empty(), size(), peek()).
  7. Ownership and lifetime rules. If you return references, document whether they remain valid after mutation. In many C++23 APIs, returning by value is safest and optimized by move semantics.

Interface vs Implementation in Modern C++

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:

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.


A C++23 Stack Interface Example

Below is a modern, self-contained example of a character stack with a clear interface. Notice what the interface includes:

#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;
}

Why This Interface Is “Correct” for C++23

This design is faithful to the ADT concept while using modern C++ idioms:

Neighboring Topics Worth Knowing

To “think like C++23” when building interfaces, keep these adjacent concepts in mind:

Summary

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.


SEMrush Software