Step 4 of 7
Pimpl
In a big codebase, a header is included by hundreds of files. Changing a class's private members changes its header, so every one of those files must recompile, and in a shared library it breaks binary compatibility with programs built against the old version. The pimpl idiom ("pointer to implementation") hides the private data behind a pointer to a struct that the header only declares.
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// ---- counter.h: what users see ----
class WordCounter {
public:
WordCounter();
~WordCounter();
void add(const std::string& w);
int distinct() const;
private:
struct Impl; // declared, not defined: "incomplete"
std::unique_ptr<Impl> impl_;
};
// ---- counter.cpp: private details, free to change ----
struct WordCounter::Impl {
std::vector<std::string> seen;
};
WordCounter::WordCounter() : impl_(std::make_unique<Impl>()) {}
WordCounter::~WordCounter() = default; // Impl is complete here
void WordCounter::add(const std::string& w) {
for (const auto& s : impl_->seen) if (s == w) return;
impl_->seen.push_back(w);
}
int WordCounter::distinct() const { return (int)impl_->seen.size(); }
int main() {
WordCounter c;
for (const char* w : {"to", "be", "or", "not", "to", "be"}) c.add(w);
std::cout << c.distinct() << "\n";
}
4
How it works
- The header only says
struct Impl;and holds astd::unique_ptr<Impl>. Users of the class can't see or depend on what's insideImpl. - The
.cppfile definesImpland every member function. You can now change the private data (switch the vector to a set, add a cache) without touching the header. - Every member function reaches the data through
impl_->....
The destructor trap
unique_ptr<Impl> must know Impl's full definition to delete it. If you let the compiler generate the destructor in the header, it's generated where Impl is incomplete, and you get a confusing error about sizeof of an incomplete type. The fix is to declare the destructor in the header and define it (even as = default) in the .cpp file, after Impl. The same applies to the move constructor and move assignment.
Your task
Define struct Roster::Impl with a std::vector<std::string>, then the constructor (create the Impl with make_unique), Roster::~Roster() = default;, Roster::Roster(Roster&&) noexcept = default;, and add and count working through impl_.
Your turn: the "header" part is written. Write the "source file" part: define Impl (holding a std::vector<std::string> of names), the constructor, destructor, move constructor, add and count.
Previous: Observers with weak_ptr Next: RAII wrappers for C libraries