Skip to content

Commit 72a5d12

Browse files
committed
refactor(memory): Split HeapResetPool out of arena.hpp
`HeapResetPool` is the only piece of the typed-API stack that reaches `std::malloc` / `std::free`. Co-hosting it with `MonotonicArena` (whose whole pitch is "zero heap usage") meant every include of `arena.hpp` pulled `<cstdlib>` along for the ride, and the heap-using class showed up in headers that have no business knowing about the heap. Move it to `note/heap_reset_pool.hpp` so the heap dependency is opt-in by include. `note/allocator.hpp` (which has the `heap_reset_allocator` factory) now pulls both headers; everything else in the public API path uses arena.hpp without dragging heap machinery in. The integration-firmware build flagged the same problem from a different angle: under the Arduino toolchain, `<cstdlib>` was not implicit and the `HeapResetPool` body failed to compile with `'malloc'/'free' is not a member of 'std'`. The split fixes that build too — heap_reset_pool.hpp explicitly `#include`s `<cstdlib>`. `HeapResetPool` stays gated by `NOTE_NO_STD_STRING == 0` so AVR / NOTE_MINIMAL builds still never compile it.
1 parent 7b7664b commit 72a5d12

3 files changed

Lines changed: 81 additions & 61 deletions

File tree

src/note/allocator.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#pragma once
2020

2121
#include <note/arena.hpp>
22+
#include <note/heap_reset_pool.hpp>
2223

2324
#include <cstddef>
2425
#include <cstdlib>

src/note/arena.hpp

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -64,65 +64,4 @@ constexpr size_t arena_cost(size_t n) {
6464

6565
} // namespace detail
6666

67-
#include <note/note_config.hpp>
68-
#if NOTE_NO_STD_STRING == 0
69-
// HeapResetPool — malloc/free with batched lifecycle. The malloc-backed
70-
// counterpart to MonotonicArena: every allocate() goes to std::malloc,
71-
// every deallocate() is a no-op, and reset() (or destruction) frees the
72-
// whole batch in one pass.
73-
//
74-
// Use when you want arena-style "drain on reset" semantics but don't want
75-
// to size a buffer up front. Common shape on desktop / Linux hosts where
76-
// the heap is fine but you still want predictable lifetime control.
77-
//
78-
// Lifetime guarantee: every pointer returned by allocate() stays valid
79-
// until reset() runs or the pool is destroyed. After reset(), all of them
80-
// are invalid.
81-
class HeapResetPool {
82-
public:
83-
HeapResetPool() = default;
84-
HeapResetPool(const HeapResetPool&) = delete;
85-
HeapResetPool& operator=(const HeapResetPool&) = delete;
86-
HeapResetPool(HeapResetPool&& o) noexcept : head_(o.head_) { o.head_ = nullptr; }
87-
HeapResetPool& operator=(HeapResetPool&& o) noexcept {
88-
if (this != &o) { reset(); head_ = o.head_; o.head_ = nullptr; }
89-
return *this;
90-
}
91-
~HeapResetPool() { reset(); }
92-
93-
void* allocate(size_t size) {
94-
// Intrusive header in each block: a next pointer that links blocks
95-
// back into the pool. Caller's payload starts after the header,
96-
// aligned to max_align_t.
97-
void* raw = std::malloc(sizeof(Block) + size);
98-
if (!raw) return nullptr;
99-
auto* b = static_cast<Block*>(raw);
100-
b->next = head_;
101-
head_ = b;
102-
return b->payload();
103-
}
104-
105-
// No-op — pool reclaims everything on reset().
106-
void deallocate(void*) {}
107-
108-
// Free every outstanding allocation. All payload pointers become invalid.
109-
void reset() {
110-
while (head_) {
111-
Block* next = head_->next;
112-
std::free(head_);
113-
head_ = next;
114-
}
115-
}
116-
117-
private:
118-
struct Block {
119-
Block* next;
120-
// Payload starts here, aligned to max_align_t.
121-
alignas(std::max_align_t) char data[1];
122-
char* payload() { return data; }
123-
};
124-
Block* head_ = nullptr;
125-
};
126-
#endif // NOTE_NO_STD_STRING == 0
127-
12867
} // namespace note

src/note/heap_reset_pool.hpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// HeapResetPool — malloc/free with batched lifecycle. The malloc-backed
2+
// counterpart to MonotonicArena: every allocate() goes to std::malloc,
3+
// every deallocate() is a no-op, and reset() (or destruction) frees the
4+
// whole batch in one pass.
5+
//
6+
// Use when you want arena-style "drain on reset" semantics but don't want
7+
// to size a buffer up front. Common shape on desktop / Linux hosts where
8+
// the heap is fine but you still want predictable lifetime control.
9+
//
10+
// Lifetime guarantee: every pointer returned by allocate() stays valid
11+
// until reset() runs or the pool is destroyed. After reset(), all of them
12+
// are invalid.
13+
//
14+
// HEAP ALERT: this header is the only place in the typed-API stack that
15+
// reaches `std::malloc` / `std::free`. It lives in a separate header so
16+
// `note/arena.hpp` and its consumers stay genuinely heap-free — pull
17+
// this in only when you explicitly want heap-backed arena semantics.
18+
// The class itself is gated by `NOTE_NO_STD_STRING == 0` so AVR/MINIMAL
19+
// builds never compile it.
20+
#pragma once
21+
22+
#include <note/note_config.hpp>
23+
24+
#if NOTE_NO_STD_STRING == 0
25+
26+
#include <cstddef>
27+
#include <cstdlib>
28+
#include <new>
29+
30+
namespace note {
31+
32+
class HeapResetPool {
33+
public:
34+
HeapResetPool() = default;
35+
HeapResetPool(const HeapResetPool&) = delete;
36+
HeapResetPool& operator=(const HeapResetPool&) = delete;
37+
HeapResetPool(HeapResetPool&& o) noexcept : head_(o.head_) { o.head_ = nullptr; }
38+
HeapResetPool& operator=(HeapResetPool&& o) noexcept {
39+
if (this != &o) { reset(); head_ = o.head_; o.head_ = nullptr; }
40+
return *this;
41+
}
42+
~HeapResetPool() { reset(); }
43+
44+
void* allocate(size_t size) {
45+
// Intrusive header in each block: a next pointer that links blocks
46+
// back into the pool. Caller's payload starts after the header,
47+
// aligned to max_align_t.
48+
void* raw = std::malloc(sizeof(Block) + size);
49+
if (!raw) return nullptr;
50+
auto* b = static_cast<Block*>(raw);
51+
b->next = head_;
52+
head_ = b;
53+
return b->payload();
54+
}
55+
56+
// No-op — pool reclaims everything on reset().
57+
void deallocate(void*) {}
58+
59+
// Free every outstanding allocation. All payload pointers become invalid.
60+
void reset() {
61+
while (head_) {
62+
Block* next = head_->next;
63+
std::free(head_);
64+
head_ = next;
65+
}
66+
}
67+
68+
private:
69+
struct Block {
70+
Block* next;
71+
// Payload starts here, aligned to max_align_t.
72+
alignas(std::max_align_t) char data[1];
73+
char* payload() { return data; }
74+
};
75+
Block* head_ = nullptr;
76+
};
77+
78+
} // namespace note
79+
80+
#endif // NOTE_NO_STD_STRING == 0

0 commit comments

Comments
 (0)