An abstraction presents the operations a caller needs while hiding implementation choices. In C++, abstract base classes, templates, and ordinary value types can all define an interface. Choose runtime polymorphism only when implementations must vary behind one stable binary or ownership boundary.
Depending on a small interface makes the caller testable without inheriting from a large concrete service.
#include <iostream>
#include <string>
#include <string_view>
#include <utility>
struct Store {
virtual ~Store() = default;
virtual void put(std::string key, std::string value) = 0;
virtual std::string get(std::string_view key) const = 0;
};
struct DemoStore : Store {
void put(std::string, std::string value) override { saved = std::move(value); }
std::string get(std::string_view) const override { return saved; }
std::string saved;
};
int main() { DemoStore store; store.put("status", "ready"); std::cout << store.get("status") << '\n'; }
ready
If every caller must know which concrete subtype exists or must call methods in a hidden sequence, the abstraction is not carrying its contract. Prefer cohesive operations and return types that represent failure explicitly.
A runtime interface usually has a public virtual destructor and pure virtual operations. Keep it small, avoid shared mutable base state, and pass ownership with smart pointers whose type states whether transfer occurs. A protected non-virtual destructor is another specialized design, but it intentionally prevents deletion through the base.
Templates provide compile-time abstraction and can preserve value semantics without virtual dispatch. Concepts can state required operations clearly. Prefer the simplest form that matches when the implementation choice is known and whether heterogeneous objects must share one collection.
An abstraction succeeds when callers can use another implementation without learning new rules. Express the smallest stable behavior in the interface, keep storage and transport details behind it, and test every implementation with the same contract suite.
Do not add getters merely to expose internals for a caller. Add the operation the caller needs, or move the responsibility when the abstraction cannot own that behavior coherently.
#include <iostream>
#include <string>
struct Notifier {
virtual ~Notifier() = default;
virtual std::string send() const = 0;
};
struct Email : Notifier { std::string send() const override { return "email"; } };
struct Console : Notifier { std::string send() const override { return "console"; } };
void deliver(const Notifier& notifier) { std::cout << notifier.send() << '\n'; }
int main() { Email email; Console console; deliver(email); deliver(console); }
email
console
The caller depends only on the behavioral contract and works unchanged with both implementations.
Try this next
0 of 2 completed
A class is abstract while it has at least one unimplemented pure virtual function.
Yes. Derived classes must still override it, but the base definition can provide shared behavior when called explicitly.
Mostly pure virtual functions and a virtual destructor, with little or no mutable state.
Explore 500+ free tutorials across 20+ languages and frameworks.