Skip to content

Commit 2f5480c

Browse files
committed
docs: update esp32 memory mgmt guidelines to prefer stack
1 parent 7e9fa25 commit 2f5480c

1 file changed

Lines changed: 167 additions & 41 deletions

File tree

esp32/best-practices/memory-management.md

Lines changed: 167 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,40 @@ Patterns for safe, efficient memory management in embedded C++.
44

55
---
66

7+
## Stack First — Heap as Fallback
8+
9+
**The fundamental rule**: Prefer stack allocation. Use heap only when stack
10+
won't work.
11+
12+
### Why stack first?
13+
14+
* **Deterministic**: No allocation failures, no fragmentation
15+
* **Fast**: No allocator overhead, just stack pointer adjustment
16+
* **Safe**: Automatic cleanup when scope exits
17+
* **Predictable**: Memory usage known at compile time
18+
19+
### When heap is acceptable
20+
21+
Use heap **only** when:
22+
23+
1. **Size unknown at compile time** (truly dynamic data)
24+
2. **Object outlives current scope** (ownership transfer)
25+
3. **Polymorphism required** (virtual dispatch needs pointer)
26+
4. **Stack would overflow** (very large buffers, >1KB typical threshold)
27+
28+
```cpp
29+
// STACK FIRST — Default choice
30+
std::array<uint8_t, 256> buffer; // Fixed size, stack
31+
CommandParser parser; // Value type, stack
32+
RGB color{255, 128, 64}; // Small struct, stack
33+
34+
// HEAP ONLY WHEN NECESSARY
35+
std::unique_ptr<LightMode> mode; // Polymorphic, needs heap
36+
std::vector<uint8_t> dynamicData(size); // Runtime size, needs heap
37+
```
38+
39+
---
40+
741
## RAII (Resource Acquisition Is Initialization)
842
943
Wrap resources in objects whose destructors clean up automatically:
@@ -86,61 +120,115 @@ public:
86120
87121
---
88122
89-
## Stack vs Heap
123+
## Stack Allocation Patterns
90124
91-
### Prefer stack allocation for small, fixed-size data
125+
### Default to stack for all fixed-size data
92126
93127
```cpp
94-
// GOOD — Stack allocated, no heap overhead
128+
// GOOD — Stack allocated, deterministic
95129
void processPacket() {
96130
std::array<uint8_t, 64> buffer;
131+
PacketHeader header;
97132
// Use buffer...
98-
} // Automatically cleaned up
133+
} // Automatic cleanup, no fragmentation
99134
100-
// AVOID — Unnecessary heap allocation
135+
// BAD — Unnecessary heap allocation
101136
void processPacket() {
102137
auto buffer = std::make_unique<std::array<uint8_t, 64>>();
103-
// ...
138+
auto header = std::make_unique<PacketHeader>();
139+
// Why heap? Fixed size, doesn't outlive scope
104140
}
105141
```
106142

107-
### Use heap for dynamic or large allocations
143+
### Stack-based class members
108144

109145
```cpp
110-
// OK — Size determined at runtime
111-
std::unique_ptr<uint8_t[]> createBuffer(size_t size) {
112-
return std::make_unique<uint8_t[]>(size);
146+
// GOOD — Members on stack (inside object)
147+
class CommandProcessor {
148+
private:
149+
std::array<uint8_t, 256> m_buffer; // Stack when object is stack
150+
CommandParser m_parser; // Value member, not pointer
151+
size_t m_bufferLen = 0;
152+
};
153+
154+
// Usage — entire object on stack
155+
void handleCommand() {
156+
CommandProcessor processor; // All members stack-allocated
157+
processor.process(data);
113158
}
159+
```
160+
161+
---
162+
163+
## When Heap Is Necessary
164+
165+
### Polymorphism requires indirection
166+
167+
```cpp
168+
// Heap needed — runtime type selection
169+
class LightController {
170+
private:
171+
std::unique_ptr<LightMode> m_currentMode; // Could be Sunrise, Candle, etc.
114172
115-
// OK — Large buffer that would overflow stack
173+
public:
174+
void setMode(ModeType type) {
175+
// Must use heap for polymorphic ownership
176+
m_currentMode = createMode(type);
177+
}
178+
};
179+
```
180+
181+
### Ownership transfer across scopes
182+
183+
```cpp
184+
// Heap needed — object outlives creating function
185+
std::unique_ptr<LightMode> createMode(ModeType type) {
186+
return std::make_unique<OwllarkSunrise>(); // Caller owns result
187+
}
188+
```
189+
190+
### Truly dynamic size
191+
192+
```cpp
193+
// Heap needed — size unknown at compile time
194+
void receiveData(size_t len) {
195+
std::vector<uint8_t> buffer(len); // Runtime size requires heap
196+
// ...
197+
}
198+
```
199+
200+
### Very large buffers
201+
202+
```cpp
203+
// Heap acceptable — would overflow typical 4-8KB task stack
116204
auto largeBuffer = std::make_unique<std::array<uint8_t, 8192>>();
117205
```
118206

119207
---
120208

121209
## Containers
122210

123-
### Use `std::vector` for dynamic arrays
211+
### Prefer `std::array` (stack) over `std::vector` (heap)
124212

125213
```cpp
126-
// BADRaw array with manual management
127-
uint8_t* data = new uint8_t[count];
128-
// ... use data
129-
delete[] data;
130-
131-
// GOOD — Vector manages memory
132-
std::vector<uint8_t> data(count);
133-
// ... use data
134-
// Automatic cleanup
214+
// BESTFixed size known at compile time → std::array (stack)
215+
std::array<uint8_t, 64> buffer;
216+
std::array<LightCommand, 10> commandHistory;
217+
218+
// ACCEPTABLE — Size truly dynamic → std::vector (heap)
219+
std::vector<uint8_t> data(runtimeSize);
220+
221+
// BAD — Using vector when size is fixed
222+
std::vector<uint8_t> buffer(64); // Why not std::array?
135223
```
136224
137-
### Use `std::array` for fixed-size arrays
225+
### Avoid C-style arrays
138226
139227
```cpp
140-
// BAD — C-style array, no bounds checking
228+
// BAD — C-style array, no bounds checking, decays to pointer
141229
uint8_t buffer[64];
142230
143-
// GOOD — std::array with bounds checking
231+
// GOOD — std::array with bounds checking, doesn't decay
144232
std::array<uint8_t, 64> buffer;
145233
buffer.at(i); // Throws on out-of-bounds
146234
```
@@ -217,12 +305,12 @@ std::shared_ptr<Config> sharedConfig;
217305

218306
## Memory-Constrained Patterns
219307

220-
### Preallocate buffers
308+
### Preallocate fixed buffers as members
221309

222310
```cpp
223311
class CommandProcessor {
224312
private:
225-
std::array<uint8_t, 256> m_buffer; // Preallocated
313+
std::array<uint8_t, 256> m_buffer; // Preallocated, no runtime allocation
226314
size_t m_bufferLen = 0;
227315

228316
public:
@@ -235,29 +323,67 @@ public:
235323
};
236324
```
237325
238-
### Use placement new for memory pools (advanced)
326+
### Fixed-capacity collections
239327
240328
```cpp
241-
// Preallocated memory pool for frequently created objects
242-
alignas(Command) std::array<uint8_t, sizeof(Command) * 10> pool;
243-
size_t poolIndex = 0;
244-
245-
Command* allocateCommand() {
246-
void* mem = &pool[poolIndex * sizeof(Command)];
247-
poolIndex++;
248-
return new(mem) Command();
249-
}
329+
// Instead of std::vector that grows dynamically
330+
template<typename T, size_t Capacity>
331+
class FixedVector {
332+
private:
333+
std::array<T, Capacity> m_data;
334+
size_t m_size = 0;
335+
336+
public:
337+
bool push_back(const T& value) {
338+
if (m_size >= Capacity) return false;
339+
m_data[m_size++] = value;
340+
return true;
341+
}
342+
// ...
343+
};
344+
345+
// Usage — all stack, no heap fragmentation
346+
FixedVector<LightCommand, 10> commandQueue;
347+
```
348+
349+
### Object pools on stack (advanced)
350+
351+
```cpp
352+
// Preallocated pool avoids repeated allocation/deallocation
353+
template<typename T, size_t Size>
354+
class ObjectPool {
355+
private:
356+
std::array<T, Size> m_objects;
357+
std::array<bool, Size> m_inUse{};
358+
359+
public:
360+
T* acquire() {
361+
for (size_t i = 0; i < Size; i++) {
362+
if (!m_inUse[i]) {
363+
m_inUse[i] = true;
364+
return &m_objects[i];
365+
}
366+
}
367+
return nullptr;
368+
}
369+
370+
void release(T* obj) {
371+
size_t idx = obj - m_objects.data();
372+
if (idx < Size) m_inUse[idx] = false;
373+
}
374+
};
250375
```
251376

252377
---
253378

254379
## Key Takeaways
255380

256-
* **Always use RAII** — resources cleaned up automatically
257-
* **Prefer `std::unique_ptr`** for owned heap objects
258-
* **Use references** for non-owning dependencies
259-
* **Stack-allocate** small, fixed-size data
260-
* **Never use raw `new`/`delete`** in application code
381+
* **Stack first** — heap is a fallback, not the default
382+
* **`std::array` over `std::vector`** when size is known
383+
* **Heap only when necessary** — polymorphism, ownership transfer, dynamic size
384+
* **RAII always** — resources cleaned up automatically
385+
* **`std::unique_ptr`** when heap is required
386+
* **Never raw `new`/`delete`** in application code
261387

262388
## Related Best Practices
263389

0 commit comments

Comments
 (0)