[core] Add TemplatableFn and TemplatableValue (value+fn) dual storage

- TemplatableFn<T, X...>: 4-byte function-pointer-only storage, used by
  TEMPLATABLE_VALUE macro for codegen-managed fields
- TemplatableValue<T, X...>: 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
This commit is contained in:
J. Nick Koston
2026-04-07 13:37:18 -10:00
parent 8cd78441a6
commit 03677db79a
6 changed files with 128 additions and 28 deletions
+1 -1
View File
@@ -275,7 +275,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
protected:
APIServer *parent_;
TemplatableValue<bool, Ts...> success_{[](Ts...) -> bool { return true; }};
TemplatableValue<bool, Ts...> success_{true};
TemplatableValue<std::string, Ts...> error_message_{""};
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
std::function<void(Ts..., JsonObject)> json_builder_;
+5 -5
View File
@@ -57,7 +57,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &service = services.emplace_next();
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
service.proto = MDNS_STR(SERVICE_TCP);
service.port = api::global_api_server->get_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<MDNSService, MDNS_SERVICE_COUN
auto &prom_service = services.emplace_next();
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP);
prom_service.port = USE_WEBSERVER_PORT;
prom_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
#endif
#ifdef USE_SENDSPIN
@@ -162,7 +162,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &sendspin_service = services.emplace_next();
sendspin_service.service_type = MDNS_STR(SERVICE_SENDSPIN);
sendspin_service.proto = MDNS_STR(SERVICE_TCP);
sendspin_service.port = USE_SENDSPIN_PORT;
sendspin_service.port = []() -> 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<MDNSService, MDNS_SERVICE_COUN
auto &web_service = services.emplace_next();
web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = USE_WEBSERVER_PORT;
web_service.port = []() -> 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<MDNSService, MDNS_SERVICE_COUN
auto &fallback_service = services.emplace_next();
fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
fallback_service.proto = MDNS_STR(SERVICE_TCP);
fallback_service.port = USE_WEBSERVER_PORT;
fallback_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}};
#endif
}
+1 -1
View File
@@ -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<uint16_t> port;
TemplatableFn<uint16_t> port;
FixedVector<MDNSTXTRecord> txt_records;
};
+2 -2
View File
@@ -63,8 +63,8 @@ class ValueRangeTrigger : public Trigger<float>, public Component {
Number *parent_;
ESPPreferenceObject rtc_;
bool previous_in_range_{false};
TemplatableValue<float, float> min_{[](float) -> float { return NAN; }}; // NAN = no bound
TemplatableValue<float, float> max_{[](float) -> float { return NAN; }}; // NAN = no bound
TemplatableFn<float, float> min_{[](float) -> float { return NAN; }};
TemplatableFn<float, float> max_{[](float) -> float { return NAN; }};
};
template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...> {
+2 -2
View File
@@ -79,8 +79,8 @@ class ValueRangeTrigger : public Trigger<float>, public Component {
Sensor *parent_;
ESPPreferenceObject rtc_;
bool previous_in_range_{false};
TemplatableValue<float, float> min_{[](float) -> float { return NAN; }};
TemplatableValue<float, float> max_{[](float) -> float { return NAN; }};
TemplatableFn<float, float> min_{[](float) -> float { return NAN; }};
TemplatableFn<float, float> max_{[](float) -> float { return NAN; }};
};
template<typename... Ts> class SensorInRangeCondition : public Condition<Ts...> {
+117 -17
View File
@@ -34,28 +34,17 @@ template<int... S> struct gens<0, S...> { using type = seq<S...>; };
#endif
// NOLINTEND(readability-identifier-naming)
#define TEMPLATABLE_VALUE_(type, name) \
protected: \
TemplatableValue<type, Ts...> name##_{}; \
\
public: \
template<typename V> 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<typename T, typename... X> class TemplatableValue {
template<typename T, typename... X> class TemplatableFn {
public:
TemplatableValue() = default;
TemplatableFn() = default;
// Accept stateless lambdas (convertible to function pointer)
template<typename F> TemplatableValue(F f) requires std::convertible_to<F, T (*)(X...)> : f_(f) {}
template<typename F> TemplatableFn(F f) requires std::convertible_to<F, T (*)(X...)> : f_(f) {}
// Reject stateful lambdas at compile time
template<typename F>
TemplatableValue(F) requires std::invocable<F, X...> &&(!std::convertible_to<F, T (*)(X...)>) = delete;
TemplatableFn(F) requires std::invocable<F, X...> &&(!std::convertible_to<F, T (*)(X...)>) = delete;
bool has_value() const { return this->f_ != nullptr; }
@@ -73,6 +62,117 @@ template<typename T, typename... X> class TemplatableValue {
T (*f_)(X...){nullptr};
};
#define TEMPLATABLE_VALUE_(type, name) \
protected: \
TemplatableFn<type, Ts...> name##_{}; \
\
public: \
template<typename V> 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<typename T, typename... X> class TemplatableValue {
public:
TemplatableValue() = default;
// Accept raw constants
template<typename V> TemplatableValue(V value) requires(!std::invocable<V, X...>) : tag_(VALUE) {
new (&this->value_) T(static_cast<T>(std::move(value)));
}
// Accept stateless lambdas (convertible to function pointer)
template<typename F> TemplatableValue(F f) requires std::convertible_to<F, T (*)(X...)> : tag_(FN) { this->f_ = f; }
// Reject stateful lambdas at compile time
template<typename F>
TemplatableValue(F) requires std::invocable<F, X...> &&(!std::convertible_to<F, T (*)(X...)>) = 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<T> 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<T>) {
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<typename... X> class TemplatableValue<std::string, X...> {