97 / 163 · C++20 · 14 min
Policy-based design: separate decisions, do not duplicate the flow
The same capacity rule can be paired with different overflow-handling approaches. Write a small policy host with member composition and C++20 concepts, distinguishing structural compatibility, semantic commitments, and compile-time configuration versus runtime replacement.
In this lesson
Modern C++ Design: Generic Programming and Design Patterns Applied
Fully read the readable body text and textual code of chapters 1–11 and Appendix A of the 2001 first edition: policies, techniques, typelist, small-object allocator, generalized functors, singleton, smart pointers, factory, abstract factory, visitor, multimethods, and threading discussion. Private full-text extraction totaled 16,122 lines, read continuously in segments with supplementary reading of the truncated range 216–554; also read the publisher’s chapters 1, 7, 8, and 11 for cross-checking. C++98 and platform threading techniques in the book are understood as historical material, not as recommended C++20 implementations.
Edition, actual reading range, and original sources →First find the stable flow and the real variation points
A batch-receiving interface allows at most cap elements. The capacity comparison is a stable rule; whether to report an error after overflow or to accept only cap elements is a replaceable decision. If a complete class is duplicated for each of the two decisions, it is easy to update only one of them when fixing the capacity logic. The host BoundedBatch should be responsible only for comparison, handing the input and the limit to the policy on overflow.
This corresponds to the book’s division of labor between host and policy, but this lesson does not need inheritance. The policy is stored as a member; there is no reason for the outside world to treat BoundedBatch as a RejectOverflow. Composition also avoids exposing a set of base-class interfaces unrelated to the identity of the business object. Only decisions that truly need to be extended become parameters; do not split every statement into a policy.
A concept checks expressions; it does not prove business semantics
OverflowPolicy requires that a const policy object can be called with two size_t arguments and returns size_t. This constraint moves diagnostics to the interface entry point, which is clearer than discovering the lack of a call operator only after deep template instantiation. However, returning a value larger than cap still satisfies the syntactic constraint, so concepts are not theorem provers.
The two policies implement throwing an exception and truncation, respectively. The host also has a semantic contract with the policy: a normal return must not exceed the limit; rejecting a request may throw. The example observes both behaviors with the same set of normal and overflow inputs, rather than only proving that the template can be instantiated. A stateful policy can also be stored as a member, but that would add state and copy contracts that need to be explained separately.
Static configuration has a clear scope of applicability
RejectOverflow and ClampOverflow produce different host types; the call is bound at compile time. This suits scenarios where the rule is determined at program-build time. If the user switches policies at runtime, ordinary branches, variant, or a virtual interface may be more direct; one must not force callers to reorganize the entire system for the sake of “zero overhead.” Template instantiations may also increase code size; whether they are worth it should be decided by actual need.
“Modern” of 2001 is not a synonym for every safe writing style of today. The book’s auto_ptr, custom copy semantics, and pointer conversions are historical discussions; modern resource management prefers standard smart pointers. This example inherits only the idea of policy decomposition, using C++20 constraints and member composition; it neither replaces standard containers nor encourages rewriting a general object framework.
Pitfalls
- A concept can only check expression constraints; whether the return value respects the capacity limit remains a semantic contract of the policy.
- Forcibly splitting two decisions that should coordinate with each other into independent policies hides illegal combinations rather than eliminating coupling.
Run an example
Minimum C++20 · complete program · Download .cpp
#include <cassert>
#include <concepts>
#include <cstddef>
#include <stdexcept>
struct RejectOverflow {
std::size_t operator()(std::size_t, std::size_t) const {
throw std::length_error("batch exceeds limit");
}
};
struct ClampOverflow {
std::size_t operator()(std::size_t, std::size_t cap) const {
return cap;
}
};
template<class P>
concept OverflowPolicy = requires(const P& p, std::size_t n) {
{ p(n, n) } -> std::same_as<std::size_t>;
};
template<OverflowPolicy P>
class BoundedBatch {
std::size_t cap_;
P policy_{};
public:
explicit BoundedBatch(std::size_t cap) : cap_(cap) {}
std::size_t accepted(std::size_t requested) const {
return requested <= cap_ ? requested : policy_(requested, cap_);
}
};
int main() {
const BoundedBatch<RejectOverflow> strict{5};
const BoundedBatch<ClampOverflow> partial{5};
assert(strict.accepted(4) == 4);
assert(partial.accepted(4) == 4);
assert(partial.accepted(8) == 5);
bool rejected = false;
try { (void)strict.accepted(8); }
catch (const std::length_error&) { rejected = true; }
assert(rejected);
}
Compile locally
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread books-modern-cpp-design.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
If you add a policy that accepts nothing on overflow, do you need to modify BoundedBatch?
Show a reference answer
No. Define struct DropOverflow { std::size_t operator()(std::size_t, std::size_t) const { return 0; } }; then instantiate BoundedBatch<DropOverflow>{5}. A request of 4 still returns 4; a request of 8 returns 0. It obeys the shared contract of “not exceeding capacity,” so the host comparison flow need not change.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.