C++ Seasoning: Three Goals for Better Code
"No raw loops. No raw synchronization primitives. No raw pointers."
Overview
"C++ Seasoning" is Sean Parent's landmark presentation from GoingNative 2013 that introduced his three fundamental goals for writing better code. This talk has become one of the most influential C++ presentations, establishing principles that guide modern C++ development.
The Three Goals
Goal 1: No Raw Loops
Principle: A raw loop is any loop inside a function where the function serves a larger purpose than implementing the algorithm represented by the loop.
Why No Raw Loops?
- Expresses intent poorly — Loops describe how, not what
- Error-prone — Off-by-one errors, iterator invalidation, boundary mistakes
- Hard to reason about — Loop invariants are implicit
- Not composable — Can't easily combine loop logic
- Hinders optimization — Compilers can better optimize known algorithms
What To Do Instead
Use standard library algorithms:
// BAD: Raw loop to find an element
for (auto i = v.begin(); i != v.end(); ++i) {
if (*i == target) {
return i;
}
}
return v.end();
// GOOD: Use std::find
return std::find(v.begin(), v.end(), target);// BAD: Raw loop to transform
std::vector<int> result;
for (const auto& x : input) {
result.push_back(x * 2);
}
// GOOD: Use std::transform
std::vector<int> result;
std::transform(input.begin(), input.end(),
std::back_inserter(result),
[](int x) { return x * 2; });
// BETTER: Use ranges (C++20)
auto result = input | std::views::transform([](int x) { return x * 2; })
| std::ranges::to<std::vector>();When Raw Loops Are Acceptable
- Implementing a new algorithm that will be reused
- Performance-critical code where measurement shows algorithms are insufficient
- The operation genuinely doesn't map to any existing algorithm
Goal 2: No Raw Synchronization Primitives
Principle: Don't use mutexes, condition variables, semaphores, or other low-level synchronization directly in application code.
Why No Raw Synchronization?
- Extremely error-prone — Deadlocks, race conditions, priority inversion
- Violates local reasoning — Must understand global state to reason about correctness
- Hard to compose — Combining locks often leads to deadlocks
- Performance pitfalls — Lock contention, false sharing
What To Do Instead
Use higher-level abstractions:
// BAD: Raw mutex and condition variable
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void producer() {
{
std::lock_guard<std::mutex> lock(mtx);
// prepare data
ready = true;
}
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
// consume data
}
// GOOD: Use futures and promises
std::promise<Data> promise;
auto future = promise.get_future();
void producer() {
Data data = prepare_data();
promise.set_value(std::move(data));
}
void consumer() {
Data data = future.get();
// consume data
}
// BETTER: Use task-based concurrency (stlab)
auto result = stlab::async(stlab::default_executor, [] {
return compute_something();
}).then([](auto value) {
return process(value);
});Preferred Concurrency Patterns
- Futures and continuations — Chain asynchronous operations
- Task queues — Submit work to thread pools
- Channels — Communicate between concurrent tasks
- Actors — Encapsulate state with message passing
Goal 3: No Raw Pointers (for Ownership)
Principle: Raw pointers should not convey ownership semantics. Use smart pointers or values instead.
Why No Raw Pointers for Ownership?
- Unclear ownership — Who deletes the memory?
- Memory leaks — Easy to forget to delete
- Double deletion — Multiple owners delete the same memory
- Dangling pointers — Using memory after it's freed
- Exception unsafety — Leaks when exceptions are thrown
What To Do Instead
// BAD: Raw pointer ownership
Widget* createWidget() {
return new Widget();
}
void useWidget() {
Widget* w = createWidget();
process(w); // What if this throws?
delete w; // Might not be reached
}
// GOOD: Use unique_ptr for exclusive ownership
std::unique_ptr<Widget> createWidget() {
return std::make_unique<Widget>();
}
void useWidget() {
auto w = createWidget();
process(w.get());
// Automatically deleted
}
// GOOD: Use shared_ptr for shared ownership
std::shared_ptr<Widget> createWidget() {
return std::make_shared<Widget>();
}
// BEST: Use values when possible
Widget createWidget() {
return Widget(); // Move semantics make this efficient
}When Raw Pointers Are Acceptable
- Non-owning references — Pointing to something owned elsewhere
- Interfacing with C APIs — External code that requires raw pointers
- Performance-critical code — After measurement proves necessity
- Optional references — When a reference might be null (though
std::optionalis often better)
The Slide and Gather Algorithms
Sean Parent introduced two algorithms that demonstrate the power of composition:
Slide
Move a range of elements to a new position. Note that this requires RandomAccessIterator for the iterator comparison (pos < first).
template<typename I> // I models RandomAccessIterator
auto slide(I first, I last, I pos) -> std::pair<I, I> {
if (pos < first) return { pos, std::rotate(pos, first, last) };
if (last < pos) return { std::rotate(first, last, pos), pos };
return { first, last };
}Gather
Collect elements matching a predicate around a position. This is the foundation for "Selection" in user interfaces (e.g., gathering selected items in a list).
template<typename I, typename P> // I models BidirectionalIterator
auto gather(I first, I last, I pos, P pred) -> std::pair<I, I> {
return {
std::stable_partition(first, pos, std::not_fn(pred)),
std::stable_partition(pos, last, pred)
};
}Key Algorithms to Know
Sean emphasizes mastering these standard algorithms as building blocks:
| Algorithm | Purpose | Complexity |
|---|---|---|
find, find_if | Locate element | O(n) |
count, count_if | Count matches | O(n) |
lower_bound, upper_bound | Binary search (sorted) | O(log n) |
transform | Apply function to range | O(n) |
accumulate, reduce | Fold operation | O(n) |
partition, stable_partition | Divide by predicate | O(n), O(n log n) |
sort, stable_sort | Order elements | O(n log n) |
rotate | Cycle elements | O(n) |
copy, move | Transfer elements | O(n) |
remove, remove_if | Prepare for erase | O(n) |
The Core Driver: Local Reasoning
The primary motivation behind these three goals is Local Reasoning. Code has local reasoning if the correctness of a function can be determined by looking at the function itself, without needing to understand the rest of the system's state or execution flow.
- No Raw Loops: Loops often hide the high-level intent and make invariants harder to see. Algorithms name the operation, enabling local reasoning about the transformation.
- No Raw Synchronization: Mutexes and condition variables in application logic require understanding all other threads that might access the same data, destroying local reasoning. Concurrency should be handled by the library/runtime.
- No Raw Pointers: Shared ownership and manual memory management create hidden dependencies across the codebase. Values and smart pointers make ownership and lifetimes explicit locally.
Beyond C++ Seasoning: The Better Code Series
While "C++ Seasoning" established the three goals, Sean Parent expanded these into a broader "Better Code" series:
Better Code: Data Structures
- Use
std::vectorby default: Most data should be in contiguous memory for cache efficiency. - Algorithms over Member Functions: Prefer standard algorithms that work on ranges over container-specific member functions where possible.
- Stability: Many UI operations rely on
stable_partitionto maintain relative order of items (like selection).
Better Code: Concurrency
- Concurrency is not Parallelism: Concurrency is about decomposing a problem into independent tasks; parallelism is about executing those tasks simultaneously.
- Avoid Threads and Mutexes: Use task-based models (like
stlab::async) to express data dependencies. - Data Flow: Think in terms of data moving through a system of tasks rather than shared state.
Better Code: Relationships
- Local Reasoning requires explicit relationships: Use value semantics to make object relationships clear.
- Parent-Child Relationships: Often best represented as a container (parent) holding its elements (children) by value.
Guidelines
- Learn the standard algorithms — Know what's available before writing loops
- Prefer algorithms over loops — Even if it seems verbose at first
- Use ranges — C++20 ranges make algorithms more composable
- Compose existing algorithms — Build new operations from primitives
- Measure before optimizing — Don't assume algorithms are slower
- Use task-based concurrency — Avoid threads and locks directly
- Prefer values to pointers — Move semantics make this efficient
- Use smart pointers for ownership —
unique_ptrby default,shared_ptrwhen needed
Anti-Patterns
Loop Anti-Patterns
// Anti-pattern: Index loop when iterator works
for (size_t i = 0; i < v.size(); ++i) {
process(v[i]);
}
// Anti-pattern: Manual find
bool found = false;
for (const auto& x : v) {
if (x == target) {
found = true;
break;
}
}
// Anti-pattern: Accumulate with loop
int sum = 0;
for (const auto& x : v) {
sum += x;
}Synchronization Anti-Patterns
// Anti-pattern: Global lock
std::mutex global_mutex;
void anyOperation() {
std::lock_guard<std::mutex> lock(global_mutex);
// everything serialized
}
// Anti-pattern: Fine-grained locking without deadlock prevention
void transfer(Account& from, Account& to, int amount) {
std::lock_guard<std::mutex> lock1(from.mutex); // Deadlock risk!
std::lock_guard<std::mutex> lock2(to.mutex);
// ...
}Pointer Anti-Patterns
// Anti-pattern: Returning raw owning pointer
Widget* createWidget(); // Who owns this?
// Anti-pattern: Raw pointer member for ownership
class Container {
Widget* widget_; // Leak on destruction?
public:
~Container() { delete widget_; } // Manual cleanup
};
// Anti-pattern: Raw pointer in container
std::vector<Widget*> widgets; // Who deletes these?References
Primary Sources
- C++ Seasoning - GoingNative 2013 — Original presentation
- C++ Seasoning Slides (PDF) — Presentation slides
- Extended Version (YouTube) — ACCU Silicon Valley version with extended content
Related Talks
- Better Code: Concurrency — Deep dive on the second goal
- Better Code: Data Structures — Using containers instead of raw pointers
- Better Code: Relationships — Managing object lifetimes
Further Reading
- C++ Core Guidelines — Industry guidelines aligned with these principles
- STLab Libraries — Sean Parent's concurrency library
"That's a rotate!" — Sean Parent's catchphrase when recognizing the rotate algorithm in disguise