Skip to content

CallbackManager: std::function Replaced with Lightweight Callback

CallbackManager and LazyCallbackManager now use a lightweight 8-byte Callback struct instead of std::function (16 bytes). External components that define their own callback registration methods using std::function should update to templates for optimal performance.

This is a breaking change for external components in ESPHome 2026.4.0 and later.

PR #14853: Replace std::function with lightweight Callback in CallbackManager

std::function has significant overhead on embedded targets: 16 bytes per instance on 32-bit platforms, plus heap allocation for captured lambdas. The new Callback struct uses C++20 if constexpr to store small trivially-copyable callables (like [this] lambdas) inline within the Callback object, alongside a function pointer, avoiding heap allocation while keeping the total size at 8 bytes.

PlatformConfigFlash DeltaHeap Delta
ESP8266minimal-160 B
ESP8266ratgdo (cover + sensors)-592 B+142 B free
ESP8266MQTT + web_server + many entities-832 B+488 B free
ESP32 IDFlarge (climate, BLE, web_server)-456 B
ESP32 IDFminimal-212 B+72 B free

All callback registration methods (add_on_*_callback, add_*_callback, register_listener, etc.) on entity base classes and components are now templates instead of taking std::function.

// Before
void add_on_state_callback(std::function<void(float)> &&callback) {
this->state_callback_.add(std::move(callback));
}
// After
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}

External components that define their own callback registration methods taking std::function.

Existing code continues to compile and work correctly. However, unconverted methods use a less efficient heap-allocation path: the lambda gets wrapped in std::function before reaching CallbackManager::add(), which then heap-allocates it. Previously the std::function was stored directly in the vector element with no extra allocation.

Callers of entity callbacks (registering callbacks on ESPHome entities) need no changes.

Convert callback registration methods from std::function to templates:

your_component.h
// Before (still compiles, but now less efficient than before)
class MyComponent : public Component {
public:
void add_on_data_callback(std::function<void(float)> &&callback) {
this->data_callback_.add(std::move(callback));
}
protected:
CallbackManager<void(float)> data_callback_;
};
your_component.h
// After (optimal — inline storage, zero heap allocation for [this] lambdas)
class MyComponent : public Component {
public:
template<typename F> void add_on_data_callback(F &&callback) {
this->data_callback_.add(std::forward<F>(callback));
}
protected:
CallbackManager<void(float)> data_callback_;
};

If you had a separate .cpp definition, move the body inline into the header and remove the .cpp definition (templates must be defined in headers).

The template approach is backward compatible — it works on older ESPHome versions too, since the template accepts std::function as well as raw lambdas. No version guard needed.

  • ESPHome 2026.4.0 (April 2026): CallbackManager uses Callback instead of std::function
  • No deprecation period — existing code still compiles but uses a less efficient path
Terminal window
# Find callback registration methods that still use std::function
grep -rn 'std::function.*&&.*callback' your_component/

If you have questions about migrating your external component, please ask in:

Comments

Feel free to leave a comment here to discuss this post with others. You can ask questions, share your experience, or suggest improvements. If you have a question about a specific feature or issue, please consider using the ESPHome Discord. Stick to English and follow ESPHome's code of conduct. These comments exist on a discussion on GitHub, so you can also comment there directly if you prefer.