From bfc5a210bcb06b4c6e18b8ada8c71434c4518245 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Mar 2026 22:20:42 -1000 Subject: [PATCH] [core] Replace std::function with lightweight Callback in CallbackManager Replace std::function with a lightweight type-erased Callback struct (8 bytes on 32-bit vs 16 for std::function) in CallbackManager and LazyCallbackManager. Using C++20 if constexpr, small trivially-copyable callables like [this] lambdas (81% of all callback registrations) are stored inline in the function pointer context without heap allocation. This eliminates std::function null checks (and __throw_bad_function_call) from all entity callback paths, and templatizes add_on_*_callback methods across all entity types so lambdas flow through without being wrapped in std::function first. Also converts EntityStateBase from hand-rolled nullable CallbackManager pointers to LazyCallbackManager for consistency. Measured flash savings: - ESP8266 simple: -160 B - ESP8266 ratgdo: -592 B - ESP32 IDF (large): -456 B - ESP32 IDF (minimal): -212 B --- .../alarm_control_panel.cpp | 16 ---- .../alarm_control_panel/alarm_control_panel.h | 31 ++++---- esphome/components/button/button.cpp | 1 - esphome/components/button/button.h | 4 +- esphome/components/climate/climate.cpp | 8 -- esphome/components/climate/climate.h | 8 +- esphome/components/cover/cover.cpp | 1 - esphome/components/cover/cover.h | 2 +- esphome/components/datetime/datetime_base.h | 4 +- .../esp32_improv/esp32_improv_component.h | 4 +- esphome/components/event/event.cpp | 4 - esphome/components/event/event.h | 4 +- .../components/factory_reset/factory_reset.h | 4 +- esphome/components/fan/fan.cpp | 1 - esphome/components/fan/fan.h | 4 +- esphome/components/lock/lock.cpp | 2 - esphome/components/lock/lock.h | 4 +- .../components/media_player/media_player.cpp | 4 - .../components/media_player/media_player.h | 4 +- esphome/components/number/number.cpp | 4 - esphome/components/number/number.h | 4 +- esphome/components/safe_mode/safe_mode.h | 4 +- esphome/components/select/select.cpp | 4 - esphome/components/select/select.h | 4 +- esphome/components/sensor/sensor.cpp | 5 -- esphome/components/sensor/sensor.h | 6 +- esphome/components/switch/switch.cpp | 3 - esphome/components/switch/switch.h | 4 +- esphome/components/text/text.cpp | 4 - esphome/components/text/text.h | 4 +- .../components/text_sensor/text_sensor.cpp | 7 -- esphome/components/text_sensor/text_sensor.h | 6 +- esphome/components/time/real_time_clock.h | 6 +- esphome/components/update/update_entity.h | 4 +- esphome/components/valve/valve.cpp | 1 - esphome/components/valve/valve.h | 2 +- esphome/core/entity_base.h | 24 +++--- esphome/core/helpers.h | 77 ++++++++++++++++--- 38 files changed, 147 insertions(+), 136 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index ab0a780cefb..fb61776532f 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -51,22 +51,6 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { } } -void AlarmControlPanel::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_cleared_callback(std::function &&callback) { - this->cleared_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_chime_callback(std::function &&callback) { - this->chime_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_ready_callback(std::function &&callback) { - this->ready_callback_.add(std::move(callback)); -} - void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code) { auto call = this->make_call(); diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index e8dc197e26f..cf99d359e75 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -37,25 +37,24 @@ class AlarmControlPanel : public EntityBase { * * @param callback The callback function */ - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } - /** Add a callback for when the state of the alarm_control_panel clears from triggered - * - * @param callback The callback function - */ - void add_on_cleared_callback(std::function &&callback); + /** Add a callback for when the state of the alarm_control_panel clears from triggered. */ + template void add_on_cleared_callback(F &&callback) { + this->cleared_callback_.add(std::forward(callback)); + } - /** Add a callback for when a chime zone goes from closed to open - * - * @param callback The callback function - */ - void add_on_chime_callback(std::function &&callback); + /** Add a callback for when a chime zone goes from closed to open. */ + template void add_on_chime_callback(F &&callback) { + this->chime_callback_.add(std::forward(callback)); + } - /** Add a callback for when a ready state changes - * - * @param callback The callback function - */ - void add_on_ready_callback(std::function &&callback); + /** Add a callback for when a ready state changes. */ + template void add_on_ready_callback(F &&callback) { + this->ready_callback_.add(std::forward(callback)); + } /** A numeric representation of the supported features as per HomeAssistant * diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index 8c06cfe59b8..b1c491805e0 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -20,6 +20,5 @@ void Button::press() { this->press_action(); this->press_callback_.call(); } -void Button::add_on_press_callback(std::function &&callback) { this->press_callback_.add(std::move(callback)); } } // namespace esphome::button diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index 0f7576a419f..96e9107532b 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -34,7 +34,9 @@ class Button : public EntityBase { * * @param callback The void() callback. */ - void add_on_press_callback(std::function &&callback); + template void add_on_press_callback(F &&callback) { + this->press_callback_.add(std::forward(callback)); + } protected: /** You should implement this virtual method if you want to create your own button. diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 43d25effa33..3f44b986dc0 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -356,14 +356,6 @@ ClimateCall &ClimateCall::set_swing_mode(optional swing_mode) return *this; } -void Climate::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - -void Climate::add_on_control_callback(std::function &&callback) { - this->control_callback_.add(std::move(callback)); -} - // Random 32bit value; If this changes existing restore preferences are invalidated static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index aa9ca91bc2c..e2cb743c0a6 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -192,7 +192,9 @@ class Climate : public EntityBase { * * @param callback The callback to call. */ - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } /** * Add a callback for the climate device configuration; each time the configuration parameters of a climate device @@ -200,7 +202,9 @@ class Climate : public EntityBase { * * @param callback The callback to call. */ - void add_on_control_callback(std::function &&callback); + template void add_on_control_callback(F &&callback) { + this->control_callback_.add(std::forward(callback)); + } /** Make a climate device control call, this is used to control the climate device, see the ClimateCall description * for more info. diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 0589aa23796..bb5965d861b 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -139,7 +139,6 @@ bool CoverCall::get_stop() const { return this->stop_; } CoverCall Cover::make_call() { return {this}; } -void Cover::add_on_state_callback(std::function &&f) { this->state_callback_.add(std::move(f)); } void Cover::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); this->tilt = clamp(this->tilt, 0.0f, 1.0f); diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 8cf9aa092aa..9a75e684871 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -125,7 +125,7 @@ class Cover : public EntityBase { /// Construct a new cover call used to control the cover. CoverCall make_call(); - void add_on_state_callback(std::function &&f); + template void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward(f)); } /** Publish the current state of the cover. * diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index 1b0b3d54639..98f23aa7137 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -14,7 +14,9 @@ class DateTimeBase : public EntityBase { public: virtual ESPTime state_as_esptime() const = 0; - void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } #ifdef USE_TIME void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 8f4cfd79581..41799f23251 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -48,8 +48,8 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { bool should_start() const { return this->should_start_; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK - void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); } #endif #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 667d4218f3c..ec63fd9c3eb 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -45,9 +45,5 @@ void Event::set_event_types(const std::vector &event_types) { this->last_event_type_ = nullptr; // Reset when types change } -void Event::add_on_event_callback(std::function &&callback) { - this->event_callback_.add(std::move(callback)); -} - } // namespace event } // namespace esphome diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 5b6a94b47c0..ebbee0bfe23 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -66,7 +66,9 @@ class Event : public EntityBase { /// Check if an event has been triggered. bool has_event() const { return this->last_event_type_ != nullptr; } - void add_on_event_callback(std::function &&callback); + template void add_on_event_callback(F &&callback) { + this->event_callback_.add(std::forward(callback)); + } protected: LazyCallbackManager event_callback_; diff --git a/esphome/components/factory_reset/factory_reset.h b/esphome/components/factory_reset/factory_reset.h index 990bb2edb66..34f89d73b60 100644 --- a/esphome/components/factory_reset/factory_reset.h +++ b/esphome/components/factory_reset/factory_reset.h @@ -17,8 +17,8 @@ class FactoryResetComponent : public Component { void dump_config() override; void setup() override; - void add_increment_callback(std::function &&callback) { - this->increment_callback_.add(std::move(callback)); + template void add_increment_callback(F &&callback) { + this->increment_callback_.add(std::forward(callback)); } protected: diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index c1e0a3dc2e1..97336e17b58 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -193,7 +193,6 @@ void Fan::apply_preset_mode_(const FanCall &call) { } } -void Fan::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } void Fan::publish_state() { auto traits = this->get_traits(); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 2caf3a712a2..e7b3681e32e 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -122,7 +122,9 @@ class Fan : public EntityBase { FanCall make_call(); /// Register a callback that will be called each time the state changes. - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } void publish_state(); diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 939c84720bf..4aa636e998c 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -48,8 +48,6 @@ void Lock::publish_state(LockState state) { #endif } -void Lock::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } - void LockCall::perform() { ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index bebd296eacb..707431d5433 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -150,7 +150,9 @@ class Lock : public EntityBase { * * @param callback The void(bool) callback. */ - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } protected: friend LockCall; diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index a53d598b0fe..70086089ff2 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -198,10 +198,6 @@ MediaPlayerCall &MediaPlayerCall::set_announcement(bool announce) { return *this; } -void MediaPlayer::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - void MediaPlayer::publish_state() { this->state_callback_.call(); #if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index 35097477181..26eca469e7f 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -155,7 +155,9 @@ class MediaPlayer : public EntityBase { void publish_state(); - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } virtual bool is_muted() const { return false; } diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index c0653c3b304..fb5d6e9f28c 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -29,8 +29,4 @@ void Number::publish_state(float state) { #endif } -void Number::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - } // namespace esphome::number diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 0425714702f..579d488cf06 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -34,7 +34,9 @@ class Number : public EntityBase { NumberCall make_call() { return NumberCall(this); } - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } NumberTraits traits; diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 1b28ea28f20..2733054962e 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -34,8 +34,8 @@ class SafeModeComponent final : public Component { void mark_successful(); #ifdef USE_SAFE_MODE_CALLBACK - void add_on_safe_mode_callback(std::function &&callback) { - this->safe_mode_callback_.add(std::move(callback)); + template void add_on_safe_mode_callback(F &&callback) { + this->safe_mode_callback_.add(std::forward(callback)); } #endif diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 91e27b30dee..df90c657e2b 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -42,10 +42,6 @@ StringRef Select::current_option() const { return this->has_state() ? StringRef(this->option_at(this->active_index_)) : StringRef(); } -void Select::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - bool Select::has_option(const std::string &option) const { return this->index_of(option.c_str()).has_value(); } bool Select::has_option(const char *option) const { return this->index_of(option).has_value(); } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index c91acd1e19e..465283d92a2 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -76,7 +76,9 @@ class Select : public EntityBase { /// Return the option value at the provided index offset (as const char* from flash). const char *option_at(size_t index) const; - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } protected: friend class SelectCall; diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index a7af6403efc..b4e59dfeb57 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -79,11 +79,6 @@ void Sensor::publish_state(float state) { #endif } -void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } -void Sensor::add_on_raw_state_callback(std::function &&callback) { - this->raw_callback_.add(std::move(callback)); -} - #ifdef USE_SENSOR_FILTER void Sensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 197896f6f68..b3bd9620364 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -111,9 +111,11 @@ class Sensor : public EntityBase { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Add a callback that will be called every time a filtered value arrives. - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { this->callback_.add(std::forward(callback)); } /// Add a callback that will be called every time the sensor sends a raw value. - void add_on_raw_state_callback(std::function &&callback); + template void add_on_raw_state_callback(F &&callback) { + this->raw_callback_.add(std::forward(callback)); + } /** This member variable stores the last state that has passed through all filters. * diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 9e9af213680..df762addbb1 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -69,9 +69,6 @@ void Switch::publish_state(bool state) { } bool Switch::assumed_state() { return false; } -void Switch::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; } bool Switch::is_inverted() const { return this->inverted_; } diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index c4f8525793a..b7761cba0a1 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -93,7 +93,9 @@ class Switch : public EntityBase { * * @param callback The void(bool) callback. */ - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } /** Returns the initial state of the switch, as persisted previously, or empty if never persisted. diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index d8ab6b1b92c..12abc5d9390 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -29,8 +29,4 @@ void Text::publish_state(const char *state, size_t len) { #endif } -void Text::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - } // namespace esphome::text diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 7d255e56880..eb6a68f9980 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -30,7 +30,9 @@ class Text : public EntityBase { /// Instantiate a TextCall object to modify this text component's state. TextCall make_call() { return TextCall(this); } - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } protected: friend class TextCall; diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 91561c5f420..aa49a85d265 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -83,13 +83,6 @@ void TextSensor::clear_filters() { } #endif // USE_TEXT_SENSOR_FILTER -void TextSensor::add_on_state_callback(std::function callback) { - this->callback_.add(std::move(callback)); -} -void TextSensor::add_on_raw_state_callback(std::function callback) { - this->raw_callback_.add(std::move(callback)); -} - const std::string &TextSensor::get_state() const { return this->state; } const std::string &TextSensor::get_raw_state() const { #ifdef USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index d26cfade966..8941790e7cc 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -62,9 +62,11 @@ class TextSensor : public EntityBase { void clear_filters(); #endif - void add_on_state_callback(std::function callback); + template void add_on_state_callback(F &&callback) { this->callback_.add(std::forward(callback)); } /// Add a callback that will be called every time the sensor sends a raw value. - void add_on_raw_state_callback(std::function callback); + template void add_on_raw_state_callback(F &&callback) { + this->raw_callback_.add(std::forward(callback)); + } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index f9de5f5614c..06ee2ea5af4 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -55,9 +55,9 @@ class RealTimeClock : public PollingComponent { /// Get the current time as the UTC epoch since January 1st 1970. time_t timestamp_now() { return ::time(nullptr); } - void add_on_time_sync_callback(std::function &&callback) { - this->time_sync_callback_.add(std::move(callback)); - }; + template void add_on_time_sync_callback(F &&callback) { + this->time_sync_callback_.add(std::forward(callback)); + } void dump_config() override; diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 82eaacaf76d..f7d0032f217 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -40,7 +40,9 @@ class UpdateEntity : public EntityBase { const UpdateInfo &update_info = update_info_; const UpdateState &state = state_; - void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } Trigger *get_update_available_trigger() { if (!update_available_trigger_) { update_available_trigger_ = std::make_unique>(); diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 493ffd8da26..636da1f3c34 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -125,7 +125,6 @@ bool ValveCall::get_stop() const { return this->stop_; } ValveCall Valve::make_call() { return {this}; } -void Valve::add_on_state_callback(std::function &&f) { this->state_callback_.add(std::move(f)); } void Valve::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index aab819a7788..b4141f5ff59 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -117,7 +117,7 @@ class Valve : public EntityBase { /// Construct a new valve call used to control the valve. ValveCall make_call(); - void add_on_state_callback(std::function &&f); + template void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward(f)); } /** Publish the current state of the valve. * diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 012a62f1c08..2dc26d81079 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -299,15 +299,11 @@ template class StatefulEntityBase : public EntityBase { virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); } void invalidate_state() { this->set_new_state({}); } - void add_full_state_callback(std::function previous, optional current)> &&callback) { - if (this->full_state_callbacks_ == nullptr) - this->full_state_callbacks_ = new CallbackManager previous, optional current)>(); // NOLINT - this->full_state_callbacks_->add(std::move(callback)); + template void add_full_state_callback(F &&callback) { + this->full_state_callbacks_.add(std::forward(callback)); } - void add_on_state_callback(std::function &&callback) { - if (this->state_callbacks_ == nullptr) - this->state_callbacks_ = new CallbackManager(); // NOLINT - this->state_callbacks_->add(std::move(callback)); + template void add_on_state_callback(F &&callback) { + this->state_callbacks_.add(std::forward(callback)); } void set_trigger_on_initial_state(bool trigger_on_initial_state) { @@ -325,21 +321,19 @@ template class StatefulEntityBase : public EntityBase { virtual bool set_new_state(const optional &new_state) { if (this->state_ != new_state) { // call the full state callbacks with the previous and new state - if (this->full_state_callbacks_ != nullptr) - this->full_state_callbacks_->call(this->state_, new_state); + this->full_state_callbacks_.call(this->state_, new_state); // trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or // the previous state was valid auto had_state = this->has_state(); this->state_ = new_state; - if (this->state_callbacks_ != nullptr && new_state.has_value() && (this->trigger_on_initial_state_ || had_state)) - this->state_callbacks_->call(new_state.value()); + if (new_state.has_value() && (this->trigger_on_initial_state_ || had_state)) + this->state_callbacks_.call(new_state.value()); return true; } return false; } bool trigger_on_initial_state_{true}; - // callbacks with full state and previous state - CallbackManager previous, optional current)> *full_state_callbacks_{}; - CallbackManager *state_callbacks_{}; + LazyCallbackManager previous, optional current)> full_state_callbacks_; + LazyCallbackManager state_callbacks_; }; } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c2f4cace9a9..062a72e7a0d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1729,6 +1729,53 @@ constexpr float fahrenheit_to_celsius(float value) { return (value - 32.0f) / 1. /// @name Utilities /// @{ +/// Lightweight type-erased callback (8 bytes on 32-bit) that avoids std::function overhead. +/// No null check, no exceptions, no heap allocation for small trivially-copyable callables. +/// +/// With C++20 if constexpr, automatically detects [this] lambdas (sizeof <= sizeof(void*), +/// trivially copyable) and stores them inline. Larger callables are heap-allocated. +template struct Callback; + +template struct Callback { + void (*fn)(void *, Ts...); + void *ctx; + + void call(Ts... args) const { this->fn(this->ctx, args...); } + + /// Create from any callable. Small trivially-copyable callables (like [this] lambdas) + /// are stored inline in the ctx pointer without heap allocation. + template static Callback create(F &&callable) { + using DecayF = std::decay_t; + if constexpr (sizeof(DecayF) <= sizeof(void *) && std::is_trivially_copyable_v) { + // Small trivial callable (e.g. [this]() { this->method(); }) - store inline in ctx + Callback cb; + cb.ctx = nullptr; + // Store the callable in the ctx pointer itself (type-punning via char* is allowed) + char *dst = reinterpret_cast(&cb.ctx); + const char *src = reinterpret_cast(&callable); + for (size_t i = 0; i < sizeof(DecayF); ++i) + dst[i] = src[i]; + cb.fn = [](void *c, Ts... args) { + // Recover the callable from the ctx pointer. + // Safe under C++20 (P0593R6): byte copy into aligned storage implicitly + // creates objects of implicit-lifetime types (trivially copyable qualifies). + alignas(DecayF) char buf[sizeof(DecayF)]; + const char *csrc = reinterpret_cast(&c); + for (size_t i = 0; i < sizeof(DecayF); ++i) + buf[i] = csrc[i]; + reinterpret_cast(buf)->operator()(args...); + }; + return cb; + } else { + // Large or non-trivial callable - heap allocate. + // Intentionally never freed: callbacks in ESPHome are registered during setup() + // and live for device lifetime. Same lifetime as the previous std::function approach. + auto *stored = new DecayF(std::forward(callable)); + return {[](void *c, Ts... args) { (*static_cast(c))(args...); }, static_cast(stored)}; + } + } +}; + template class CallbackManager; /** Helper class to allow having multiple subscribers to a callback. @@ -1737,13 +1784,14 @@ template class CallbackManager; */ template class CallbackManager { public: - /// Add a callback to the list. - void add(std::function &&callback) { this->callbacks_.push_back(std::move(callback)); } + /// Add any callable. Small trivially-copyable callables (like [this] lambdas) + /// are stored inline without heap allocation or std::function. + template void add(F &&callback) { this->add_(Callback::create(std::forward(callback))); } - /// Call all callbacks in this manager. + /// Call all callbacks in this manager. No null check on invoke. void call(Ts... args) { for (auto &cb : this->callbacks_) - cb(args...); + cb.call(args...); } size_t size() const { return this->callbacks_.size(); } @@ -1751,7 +1799,10 @@ template class CallbackManager { void operator()(Ts... args) { call(args...); } protected: - std::vector> callbacks_; + template friend class LazyCallbackManager; + /// Non-template core to avoid code duplication per lambda type. + void add_(Callback cb) { this->callbacks_.push_back(cb); } + std::vector> callbacks_; }; template class LazyCallbackManager; @@ -1784,13 +1835,8 @@ template class LazyCallbackManager { LazyCallbackManager(LazyCallbackManager &&) = delete; LazyCallbackManager &operator=(LazyCallbackManager &&) = delete; - /// Add a callback to the list. Allocates the underlying CallbackManager on first use. - void add(std::function &&callback) { - if (!this->callbacks_) { - this->callbacks_ = new CallbackManager(); - } - this->callbacks_->add(std::move(callback)); - } + /// Add any callable. Allocates the underlying CallbackManager on first use. + template void add(F &&callback) { this->add_(Callback::create(std::forward(callback))); } /// Call all callbacks in this manager. No-op if no callbacks registered. void call(Ts... args) { @@ -1809,6 +1855,13 @@ template class LazyCallbackManager { void operator()(Ts... args) { this->call(args...); } protected: + /// Non-template core to avoid code duplication per lambda type. + void add_(Callback cb) { + if (!this->callbacks_) { + this->callbacks_ = new CallbackManager(); + } + this->callbacks_->add_(cb); + } CallbackManager *callbacks_{nullptr}; };