From 03677db79a5f07a37de478e28587f1da980dc40a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 13:37:18 -1000 Subject: [PATCH] [core] Add TemplatableFn and TemplatableValue (value+fn) dual storage - TemplatableFn: 4-byte function-pointer-only storage, used by TEMPLATABLE_VALUE macro for codegen-managed fields - TemplatableValue: 8-byte value-or-function-pointer storage, backward compatible with raw constant init and assignment - Revert api/user_services.h to original (uses TemplatableValue) - Optimize sensor/number min_/max_ to TemplatableFn - Optimize mdns port to TemplatableFn with lambda wrappers --- esphome/components/api/user_services.h | 2 +- esphome/components/mdns/mdns_component.cpp | 10 +- esphome/components/mdns/mdns_component.h | 2 +- esphome/components/number/automation.h | 4 +- esphome/components/sensor/automation.h | 4 +- esphome/core/automation.h | 134 ++++++++++++++++++--- 6 files changed, 128 insertions(+), 28 deletions(-) diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 1f35be7ef9..d1b8a6ef0d 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -275,7 +275,7 @@ template class APIRespondAction : public Action { protected: APIServer *parent_; - TemplatableValue success_{[](Ts...) -> bool { return true; }}; + TemplatableValue success_{true}; TemplatableValue error_message_{""}; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON std::function json_builder_; diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 342a6e6c64..b5a70449cf 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -57,7 +57,7 @@ void MDNSComponent::compile_records_(StaticVectorget_port(); + service.port = []() -> uint16_t { return api::global_api_server->get_port(); }; const auto &friendly_name = App.get_friendly_name(); bool friendly_name_empty = friendly_name.empty(); @@ -151,7 +151,7 @@ void MDNSComponent::compile_records_(StaticVector uint16_t { return USE_WEBSERVER_PORT; }; #endif #ifdef USE_SENDSPIN @@ -162,7 +162,7 @@ void MDNSComponent::compile_records_(StaticVector uint16_t { return USE_SENDSPIN_PORT; }; sendspin_service.txt_records = {{MDNS_STR(TXT_SENDSPIN_PATH), MDNS_STR(VALUE_SENDSPIN_PATH)}}; #endif @@ -172,7 +172,7 @@ void MDNSComponent::compile_records_(StaticVector uint16_t { return USE_WEBSERVER_PORT; }; #endif #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_SENDSPIN) && !defined(USE_WEBSERVER) && \ @@ -185,7 +185,7 @@ void MDNSComponent::compile_records_(StaticVector uint16_t { return USE_WEBSERVER_PORT; }; fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}}; #endif } diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 47cad4bf71..adf88a9cf1 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -36,7 +36,7 @@ struct MDNSService { // second label indicating protocol _including_ underscore character prefix // as defined in RFC6763 Section 7, like "_tcp" or "_udp" const MDNSString *proto; - TemplatableValue port; + TemplatableFn port; FixedVector txt_records; }; diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 834998b32b..2843aa6bf5 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -63,8 +63,8 @@ class ValueRangeTrigger : public Trigger, public Component { Number *parent_; ESPPreferenceObject rtc_; bool previous_in_range_{false}; - TemplatableValue min_{[](float) -> float { return NAN; }}; // NAN = no bound - TemplatableValue max_{[](float) -> float { return NAN; }}; // NAN = no bound + TemplatableFn min_{[](float) -> float { return NAN; }}; + TemplatableFn max_{[](float) -> float { return NAN; }}; }; template class NumberInRangeCondition : public Condition { diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 989ee2317b..37578f5320 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -79,8 +79,8 @@ class ValueRangeTrigger : public Trigger, public Component { Sensor *parent_; ESPPreferenceObject rtc_; bool previous_in_range_{false}; - TemplatableValue min_{[](float) -> float { return NAN; }}; - TemplatableValue max_{[](float) -> float { return NAN; }}; + TemplatableFn min_{[](float) -> float { return NAN; }}; + TemplatableFn max_{[](float) -> float { return NAN; }}; }; template class SensorInRangeCondition : public Condition { diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 7879478f5b..a89a16aa0a 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -34,28 +34,17 @@ template struct gens<0, S...> { using type = seq; }; #endif // NOLINTEND(readability-identifier-naming) -#define TEMPLATABLE_VALUE_(type, name) \ - protected: \ - TemplatableValue name##_{}; \ -\ - public: \ - template void set_##name(V name) { this->name##_ = name; } - -#define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name) - -/// Primary template: function-pointer-only storage (4 bytes on 32-bit). +/// Function-pointer-only templatable storage (4 bytes on 32-bit). +/// Used by the TEMPLATABLE_VALUE macro for codegen-managed fields. /// Codegen wraps constants in stateless lambdas so only a function pointer is needed. -/// Stateful lambdas (std::function) are rejected at compile time. -template class TemplatableValue { +template class TemplatableFn { public: - TemplatableValue() = default; + TemplatableFn() = default; - // Accept stateless lambdas (convertible to function pointer) - template TemplatableValue(F f) requires std::convertible_to : f_(f) {} + template TemplatableFn(F f) requires std::convertible_to : f_(f) {} - // Reject stateful lambdas at compile time template - TemplatableValue(F) requires std::invocable &&(!std::convertible_to) = delete; + TemplatableFn(F) requires std::invocable &&(!std::convertible_to) = delete; bool has_value() const { return this->f_ != nullptr; } @@ -73,6 +62,117 @@ template class TemplatableValue { T (*f_)(X...){nullptr}; }; +#define TEMPLATABLE_VALUE_(type, name) \ + protected: \ + TemplatableFn name##_{}; \ +\ + public: \ + template void set_##name(V name) { this->name##_ = name; } + +#define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name) + +/// Primary TemplatableValue: stores either a constant value or a function pointer. +/// No std::function, no string-specific paths. 8 bytes on 32-bit. +/// Accepts raw constants for backward compatibility with direct C++ usage. +template class TemplatableValue { + public: + TemplatableValue() = default; + + // Accept raw constants + template TemplatableValue(V value) requires(!std::invocable) : tag_(VALUE) { + new (&this->value_) T(static_cast(std::move(value))); + } + + // Accept stateless lambdas (convertible to function pointer) + template TemplatableValue(F f) requires std::convertible_to : tag_(FN) { this->f_ = f; } + + // Reject stateful lambdas at compile time + template + TemplatableValue(F) requires std::invocable &&(!std::convertible_to) = delete; + + TemplatableValue(const TemplatableValue &other) : tag_(other.tag_) { + if (this->tag_ == VALUE) { + new (&this->value_) T(other.value_); + } else if (this->tag_ == FN) { + this->f_ = other.f_; + } + } + + TemplatableValue(TemplatableValue &&other) noexcept : tag_(other.tag_) { + if (this->tag_ == VALUE) { + new (&this->value_) T(std::move(other.value_)); + } else if (this->tag_ == FN) { + this->f_ = other.f_; + } + other.tag_ = NONE; + } + + TemplatableValue &operator=(const TemplatableValue &other) { + if (this != &other) { + this->destroy_(); + this->tag_ = other.tag_; + if (this->tag_ == VALUE) { + new (&this->value_) T(other.value_); + } else if (this->tag_ == FN) { + this->f_ = other.f_; + } + } + return *this; + } + + TemplatableValue &operator=(TemplatableValue &&other) noexcept { + if (this != &other) { + this->destroy_(); + this->tag_ = other.tag_; + if (this->tag_ == VALUE) { + new (&this->value_) T(std::move(other.value_)); + } else if (this->tag_ == FN) { + this->f_ = other.f_; + } + other.tag_ = NONE; + } + return *this; + } + + ~TemplatableValue() { this->destroy_(); } + + bool has_value() const { return this->tag_ != NONE; } + + T value(X... x) const { + if (this->tag_ == FN) + return this->f_(x...); + if (this->tag_ == VALUE) + return this->value_; + return T{}; + } + + optional optional_value(X... x) const { + if (this->tag_ == NONE) + return {}; + return this->value(x...); + } + + T value_or(X... x, T default_value) const { + if (this->tag_ == NONE) + return default_value; + return this->value(x...); + } + + protected: + void destroy_() { + if constexpr (!std::is_trivially_destructible_v) { + if (this->tag_ == VALUE) + this->value_.~T(); + } + } + + enum Tag : uint8_t { NONE, VALUE, FN } tag_{NONE}; + union { + T value_; + T (*f_)(X...); + }; +}; + /// Specialization for std::string: supports VALUE, STATIC_STRING, FLASH_STRING, /// stateless lambdas, and stateful lambdas (std::function). template class TemplatableValue {