2026-05-29
The asker is working in an ESP-IDF/ESPHome environment where C++ exceptions are disabled at compile time (-fno-exceptions). They need to resize() a std::vector to a size dictated by an untrusted frame header, and recover gracefully if the device doesn't have enough heap to satisfy the request. Normally std::vector::resize() throws std::bad_alloc on failure, but with exceptions off, libstdc++ / libc++ typically calls std::abort() instead — which on an ESP32 means a watchdog reset. Not great for a robust protocol handler.
Why it's tricky: the STL was designed around exceptions for failure signaling. Once you remove that channel, the allocator's failure path becomes a one-way trip to abort. You can't "try" a resize and check a return code, because there is no return code. And you can't catch what was never thrown.
Approaches, roughly in order of cleanliness:
heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) (or esp_get_free_heap_size()) before the resize. If it's smaller than new_size * sizeof(T) plus a safety margin for fragmentation, reject the frame with FPC_RESULT_OUT_OF_MEMORY. Cheap and portable-enough within the ESP ecosystem.new (std::nothrow) uint8_t[n] or heap_caps_malloc, check for nullptr, then either construct the vector around the buffer (via a custom allocator) or just keep using the raw buffer. For a payload buffer this is often the right answer — you didn't need vector semantics anyway.std::nothrow semantics. Write an allocator whose allocate() returns nullptr instead of calling abort. The catch: the standard says allocate must throw on failure, and a returned null will trip undefined behavior inside vector's internals. So this only works reliably if you also override resize's call path — which you can't.new_handler. Install a handler via std::set_new_handler that uses setjmp/longjmp to escape. Works, but longjmp-ing out of an allocation leaves the vector in an indeterminate state, so you must longjmp all the way past the vector's lifetime. Ugly but viable for top-level frame handlers.Gotchas: resize() may allocate more than new_size due to capacity growth strategy — call reserve() with exact size first, or use a std::vector with a shrinking strategy. Heap fragmentation on ESP32 means a "successful" pre-flight check can still fail moments later if another task allocates. And PSRAM-backed allocations need MALLOC_CAP_SPIRAM — easy to miss when the default heap is small.
