Skip to content

Commit d6fe2a1

Browse files
LessUpCopilot
andauthored
hardening: close header-only safety gaps (#2)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 17f2905 commit d6fe2a1

14 files changed

Lines changed: 257 additions & 42 deletions

File tree

examples/03-modern-cpp/include/buffer.hpp

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class Buffer {
8686
}
8787

8888
size_t size() const { return size_; }
89+
bool empty() const { return size_ == 0 || data_ == nullptr; }
8990
char* data() { return data_; }
9091
const char* data() const { return data_; }
9192

@@ -104,6 +105,15 @@ class Buffer {
104105
}
105106
};
106107

108+
inline void observe_buffer(const Buffer& buf) {
109+
if (buf.empty()) {
110+
return;
111+
}
112+
113+
volatile char c = buf.data()[0];
114+
(void)c;
115+
}
116+
107117
//------------------------------------------------------------------------------
108118
// Functions demonstrating copy vs move
109119
//------------------------------------------------------------------------------
@@ -112,18 +122,14 @@ class Buffer {
112122
* @brief Process buffer by copy (expensive)
113123
*/
114124
inline void process_by_copy(Buffer buf) {
115-
// Do something with buf
116-
volatile char c = buf.data()[0];
117-
(void)c;
125+
observe_buffer(buf);
118126
}
119127

120128
/**
121129
* @brief Process buffer by const reference (no copy)
122130
*/
123131
inline void process_by_ref(const Buffer& buf) {
124-
// Do something with buf
125-
volatile char c = buf.data()[0];
126-
(void)c;
132+
observe_buffer(buf);
127133
}
128134

129135
} // namespace hpc::move_semantics

examples/03-modern-cpp/include/compile_time.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace hpc::compile_time {
2626
/**
2727
* @brief Runtime factorial (for comparison)
2828
*/
29-
int64_t factorial_runtime(int n) {
29+
inline int64_t factorial_runtime(int n) {
3030
int64_t result = 1;
3131
for (int i = 2; i <= n; ++i) {
3232
result *= i;

examples/03-modern-cpp/include/ranges_utils.hpp

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ namespace hpc::ranges {
2929
/**
3030
* @brief Transform using raw loop
3131
*/
32-
void transform_raw_loop(const std::vector<int>& input, std::vector<int>& output) {
32+
inline void transform_raw_loop(const std::vector<int>& input, std::vector<int>& output) {
3333
output.resize(input.size());
3434
for (size_t i = 0; i < input.size(); ++i) {
3535
output[i] = input[i] * 2 + 1;
@@ -39,15 +39,15 @@ void transform_raw_loop(const std::vector<int>& input, std::vector<int>& output)
3939
/**
4040
* @brief Transform using std::transform
4141
*/
42-
void transform_algorithm(const std::vector<int>& input, std::vector<int>& output) {
42+
inline void transform_algorithm(const std::vector<int>& input, std::vector<int>& output) {
4343
output.resize(input.size());
4444
std::transform(input.begin(), input.end(), output.begin(), [](int x) { return x * 2 + 1; });
4545
}
4646

4747
/**
4848
* @brief Transform using ranges
4949
*/
50-
void transform_ranges(const std::vector<int>& input, std::vector<int>& output) {
50+
inline void transform_ranges(const std::vector<int>& input, std::vector<int>& output) {
5151
output.resize(input.size());
5252
std::ranges::transform(input, output.begin(), [](int x) { return x * 2 + 1; });
5353
}
@@ -59,7 +59,7 @@ void transform_ranges(const std::vector<int>& input, std::vector<int>& output) {
5959
/**
6060
* @brief Filter using raw loop
6161
*/
62-
std::vector<int> filter_raw_loop(const std::vector<int>& input) {
62+
inline std::vector<int> filter_raw_loop(const std::vector<int>& input) {
6363
std::vector<int> output;
6464
output.reserve(input.size() / 2); // Estimate
6565
for (int x : input) {
@@ -73,7 +73,7 @@ std::vector<int> filter_raw_loop(const std::vector<int>& input) {
7373
/**
7474
* @brief Filter using std::copy_if
7575
*/
76-
std::vector<int> filter_algorithm(const std::vector<int>& input) {
76+
inline std::vector<int> filter_algorithm(const std::vector<int>& input) {
7777
std::vector<int> output;
7878
output.reserve(input.size() / 2);
7979
std::copy_if(input.begin(), input.end(), std::back_inserter(output),
@@ -84,7 +84,7 @@ std::vector<int> filter_algorithm(const std::vector<int>& input) {
8484
/**
8585
* @brief Filter using ranges view (lazy)
8686
*/
87-
auto filter_ranges_view(const std::vector<int>& input) {
87+
inline auto filter_ranges_view(const std::vector<int>& input) {
8888
return input | std::views::filter([](int x) { return x % 2 == 0; });
8989
}
9090

@@ -95,7 +95,7 @@ auto filter_ranges_view(const std::vector<int>& input) {
9595
/**
9696
* @brief Filter then transform using raw loops
9797
*/
98-
std::vector<int> chain_raw_loop(const std::vector<int>& input) {
98+
inline std::vector<int> chain_raw_loop(const std::vector<int>& input) {
9999
std::vector<int> output;
100100
output.reserve(input.size() / 2);
101101
for (int x : input) {
@@ -109,7 +109,7 @@ std::vector<int> chain_raw_loop(const std::vector<int>& input) {
109109
/**
110110
* @brief Filter then transform using ranges (lazy, single pass)
111111
*/
112-
auto chain_ranges_view(const std::vector<int>& input) {
112+
inline auto chain_ranges_view(const std::vector<int>& input) {
113113
return input | std::views::filter([](int x) { return x % 2 == 0; }) |
114114
std::views::transform([](int x) { return x * 2 + 1; });
115115
}
@@ -133,7 +133,7 @@ std::vector<std::ranges::range_value_t<R>> to_vector(R&& range) {
133133
/**
134134
* @brief Sum using raw loop
135135
*/
136-
int64_t sum_raw_loop(const std::vector<int>& input) {
136+
inline int64_t sum_raw_loop(const std::vector<int>& input) {
137137
int64_t sum = 0;
138138
for (int x : input) {
139139
sum += x;
@@ -144,7 +144,7 @@ int64_t sum_raw_loop(const std::vector<int>& input) {
144144
/**
145145
* @brief Sum using std::accumulate
146146
*/
147-
int64_t sum_algorithm(const std::vector<int>& input) {
147+
inline int64_t sum_algorithm(const std::vector<int>& input) {
148148
return std::accumulate(input.begin(), input.end(), int64_t{0});
149149
}
150150

@@ -154,7 +154,7 @@ int64_t sum_algorithm(const std::vector<int>& input) {
154154
* C++23 introduces std::ranges::fold_left for this purpose.
155155
* In C++20 we iterate over a ranges::subrange to stay within the ranges API.
156156
*/
157-
int64_t sum_ranges(const std::vector<int>& input) {
157+
inline int64_t sum_ranges(const std::vector<int>& input) {
158158
int64_t sum = 0;
159159
for (int x : std::ranges::subrange(input)) {
160160
sum += x;

examples/05-concurrency/include/lock_free_queue.hpp

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <atomic>
2020
#include <optional>
2121
#include <thread>
22+
#include <utility>
2223

2324
#include "concurrency_utils.hpp"
2425

@@ -150,7 +151,7 @@ class MPMCQueue {
150151

151152
struct Cell {
152153
std::atomic<size_t> sequence;
153-
T data;
154+
std::optional<T> data;
154155
};
155156

156157
public:
@@ -166,70 +167,84 @@ class MPMCQueue {
166167
* @return true if successful, false if queue is full
167168
*/
168169
bool push(const T& value) {
170+
return push_impl(value);
171+
}
172+
173+
/**
174+
* @brief Push an element with move semantics (thread-safe)
175+
* @param value Element to push
176+
* @return true if successful, false if queue is full
177+
*/
178+
bool push(T&& value) {
179+
return push_impl(std::move(value));
180+
}
181+
182+
/**
183+
* @brief Pop an element from the queue (thread-safe)
184+
* @return optional containing the value, or empty if queue is empty
185+
*/
186+
std::optional<T> pop() {
169187
Cell* cell;
170-
size_t pos = enqueue_pos_.load(std::memory_order_relaxed);
188+
size_t pos = dequeue_pos_.load(std::memory_order_relaxed);
171189
int backoff = 1;
172190

173191
for (;;) {
174192
cell = &cells_[pos & MASK];
175193
size_t seq = cell->sequence.load(std::memory_order_acquire);
176-
intptr_t diff = static_cast<intptr_t>(seq) - static_cast<intptr_t>(pos);
194+
intptr_t diff = static_cast<intptr_t>(seq) - static_cast<intptr_t>(pos + 1);
177195

178196
if (diff == 0) {
179-
if (enqueue_pos_.compare_exchange_weak(pos, pos + 1, std::memory_order_relaxed)) {
197+
if (dequeue_pos_.compare_exchange_weak(pos, pos + 1, std::memory_order_relaxed)) {
180198
break;
181199
}
182200
} else if (diff < 0) {
183-
return false;
201+
return std::nullopt;
184202
} else {
185203
for (int i = 0; i < backoff; ++i) {
186204
std::this_thread::yield();
187205
}
188206
backoff = std::min(backoff * 2, 64);
189-
pos = enqueue_pos_.load(std::memory_order_relaxed);
207+
pos = dequeue_pos_.load(std::memory_order_relaxed);
190208
}
191209
}
192210

193-
cell->data = value;
194-
cell->sequence.store(pos + 1, std::memory_order_release);
195-
return true;
211+
T value = std::move(*cell->data);
212+
cell->data.reset();
213+
cell->sequence.store(pos + Capacity, std::memory_order_release);
214+
return value;
196215
}
197216

198-
/**
199-
* @brief Pop an element from the queue (thread-safe)
200-
* @return optional containing the value, or empty if queue is empty
201-
*/
202-
std::optional<T> pop() {
217+
private:
218+
template <typename U>
219+
bool push_impl(U&& value) {
203220
Cell* cell;
204-
size_t pos = dequeue_pos_.load(std::memory_order_relaxed);
221+
size_t pos = enqueue_pos_.load(std::memory_order_relaxed);
205222
int backoff = 1;
206223

207224
for (;;) {
208225
cell = &cells_[pos & MASK];
209226
size_t seq = cell->sequence.load(std::memory_order_acquire);
210-
intptr_t diff = static_cast<intptr_t>(seq) - static_cast<intptr_t>(pos + 1);
227+
intptr_t diff = static_cast<intptr_t>(seq) - static_cast<intptr_t>(pos);
211228

212229
if (diff == 0) {
213-
if (dequeue_pos_.compare_exchange_weak(pos, pos + 1, std::memory_order_relaxed)) {
230+
if (enqueue_pos_.compare_exchange_weak(pos, pos + 1, std::memory_order_relaxed)) {
214231
break;
215232
}
216233
} else if (diff < 0) {
217-
return std::nullopt;
234+
return false;
218235
} else {
219236
for (int i = 0; i < backoff; ++i) {
220237
std::this_thread::yield();
221238
}
222239
backoff = std::min(backoff * 2, 64);
223-
pos = dequeue_pos_.load(std::memory_order_relaxed);
240+
pos = enqueue_pos_.load(std::memory_order_relaxed);
224241
}
225242
}
226243

227-
T value = std::move(cell->data);
228-
cell->sequence.store(pos + Capacity, std::memory_order_release);
229-
return value;
244+
cell->data.emplace(std::forward<U>(value));
245+
cell->sequence.store(pos + 1, std::memory_order_release);
246+
return true;
230247
}
231-
232-
private:
233248
static constexpr size_t MASK = Capacity - 1;
234249

235250
alignas(hpc::core::CACHE_LINE_SIZE) Cell cells_[Capacity];
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Design: Header-Only Hardening
2+
3+
## Overview
4+
5+
This change deepens three fragile modules by moving hidden invariants behind their own interfaces instead of forcing callers and future maintainers to remember them.
6+
7+
## Design Decisions
8+
9+
### 1. `MPMCQueue` owns payload lifetime explicitly
10+
11+
The queue cell will store `std::optional<T>` instead of a permanently live `T`. This removes the accidental default-constructible requirement and gives the module a real interface for generic payloads. The queue will also expose an rvalue enqueue path so move-only and expensive-to-copy types can cross the seam safely.
12+
13+
### 2. `Buffer` processing absorbs the empty-buffer invariant
14+
15+
The move-semantics example currently requires callers to know that `buf.data()[0]` is only valid when `size() > 0`. That is a shallow interface. The hardening change moves the empty-buffer check into the processing helpers so the module remains safe for default-constructed and zero-sized buffers.
16+
17+
### 3. Modern C++ example headers become explicitly link-safe
18+
19+
The repository treats these example headers as header-only modules. Non-template free functions defined in headers will be marked `inline`, and a dedicated multi-translation-unit smoke target will prove the contract during normal builds.
20+
21+
## Testing Strategy
22+
23+
1. Add a failing queue test that uses non-default-constructible and move-only payloads.
24+
2. Add a failing move-semantics test that exercises empty buffers.
25+
3. Add a failing multi-translation-unit smoke target for the modern C++ headers.
26+
4. Apply the minimal production changes to make each loop pass.
27+
5. Re-run the full preset-driven debug validation path.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Proposal: Header-Only Hardening
2+
3+
## Summary
4+
5+
Harden the header-only teaching modules where the current interface promises more than the implementation can safely deliver, focusing on queue payload support, empty-buffer safety, and multi-translation-unit link safety.
6+
7+
## Motivation
8+
9+
The repository is in closure and hardening mode. Several teaching modules already pass today's tests, but still carry design drift that weakens long-term maintainability:
10+
11+
1. `MPMCQueue` documents generic payload support, yet its implementation requires default-constructible element types and does not expose a move-oriented enqueue path.
12+
2. The move-semantics example leaks a hidden caller invariant: `process_by_copy` and `process_by_ref` dereference element zero even when the buffer is empty.
13+
3. Header-defined non-template functions in the modern C++ examples are not explicitly `inline`, which makes the header-only contract fragile across multiple translation units.
14+
15+
## Scope
16+
17+
- Harden the concurrency queue interface so the implementation matches its documented payload story.
18+
- Harden the move-semantics example against empty-buffer use.
19+
- Enforce link-safe header-only behavior in modern C++ example headers.
20+
- Add targeted regression coverage for each fix.
21+
22+
## Out of Scope
23+
24+
- New performance claims or benchmark rewrites.
25+
- Broad API redesign across unrelated modules.
26+
- Reworking the docs site or unrelated repository governance surfaces.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Concurrency and Multithreading
2+
3+
## ADDED Requirements
4+
5+
### Requirement: Queue Payload Lifetime Safety
6+
7+
THE Example_Module SHALL allow the lock-free queue interface to store payload types without requiring a default constructor.
8+
9+
#### Scenario: MPMC queue accepts non-default-constructible payloads
10+
11+
- **WHEN** a maintainer instantiates `MPMCQueue<T, N>` with a payload type that is move-constructible but has no default constructor
12+
- **THEN** the queue builds successfully and preserves payload values through push/pop operations
13+
14+
#### Scenario: MPMC queue preserves move-only payloads
15+
16+
- **WHEN** a maintainer enqueues move-only payloads into `MPMCQueue<T, N>`
17+
- **THEN** the queue transfers ownership correctly and pops each payload exactly once
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Modern C++ Features
2+
3+
## ADDED Requirements
4+
5+
### Requirement: Header-Only Example Safety
6+
7+
THE Example_Module SHALL keep header-defined utilities safe to include from multiple translation units.
8+
9+
#### Scenario: Header-only examples link from multiple translation units
10+
11+
- **WHEN** the modern C++ example headers are included from more than one translation unit in the same target
12+
- **THEN** the target links successfully without duplicate symbol errors
13+
14+
#### Scenario: Empty buffers are safe to observe
15+
16+
- **WHEN** the move-semantics helper functions process a default-constructed or zero-sized `Buffer`
17+
- **THEN** they do not dereference invalid memory and complete without crashing
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Tasks: Header-Only Hardening
2+
3+
## 1. Queue payload hardening
4+
5+
- [x] Add regression coverage showing `MPMCQueue` handles non-default-constructible and move-only payloads.
6+
- [x] Refactor queue cell storage so element lifetime matches the queue seam.
7+
- [x] Add move enqueue support without weakening existing behavior.
8+
9+
## 2. Empty-buffer safety
10+
11+
- [x] Add regression coverage for processing default-constructed and zero-sized buffers.
12+
- [x] Move the empty-buffer invariant behind the move-semantics helper interface.
13+
14+
## 3. Header-only link safety
15+
16+
- [x] Add a multi-translation-unit smoke target that includes the modern C++ headers from more than one source file.
17+
- [x] Mark header-defined non-template free functions `inline` where needed.
18+
19+
## 4. Validation
20+
21+
- [x] Run the targeted red/green loops for the concurrency and modern C++ test surfaces.
22+
- [x] Run `cmake --preset=debug && cmake --build build/debug && ctest --preset=debug`.

0 commit comments

Comments
 (0)