@@ -121,6 +121,20 @@ concept string_convertible = !exact_value_type<T> &&
121121 !floating_convertible<T> &&
122122 std::convertible_to<T, std::string>;
123123
124+ // Generic concept for types with operator<< - must be last fallback
125+ // Note: operator<< returns std::basic_ostream<CharT, Traits>& (not stringstream&),
126+ // so we check that it returns something convertible to std::ostream&
127+ template<typename T>
128+ concept streamable = !exact_value_type<T> &&
129+ !c_string_type<T> &&
130+ !string_view_convertible<T> &&
131+ !integral_convertible<T> &&
132+ !floating_convertible<T> &&
133+ !string_convertible<T> &&
134+ requires(std::stringstream& ss, const T& v) {
135+ { ss << v } -> std::convertible_to<std::ostream&>;
136+ };
137+
124138struct level_manip {
125139 syslog::severity sev;
126140 std::string_view msg_id;
@@ -246,14 +260,23 @@ private:
246260 return std::forward<T>(v);
247261 }
248262
263+ // C-strings (const char*, char*) are handled separately even though they're convertible
264+ // to std::string, because:
265+ // 1. Template overload resolution: more specific (c_string_type) is checked first
266+ // 2. Concept dependencies: c_string_type is used to exclude C-strings from other
267+ // concepts (string_view_convertible, integral_convertible, etc.)
268+ // 3. Clear intent: explicit handling for C-strings vs. other string-like types
269+ // Both do the same conversion (std::string{v}), but the separation ensures proper
270+ // ordering and prevents C-strings from matching string_view_convertible or other
271+ // concepts in the chain
249272 template<c_string_type T>
250273 static value convert_to_value(T v) {
251- return std::string(v) ;
274+ return std::string{v} ;
252275 }
253276
254277 template<string_view_convertible T>
255278 static value convert_to_value(T&& v) {
256- return std::string(v) ;
279+ return std::string{v} ;
257280 }
258281
259282 template<integral_convertible T>
@@ -266,25 +289,29 @@ private:
266289 return static_cast<double>(v);
267290 }
268291
269- // Special handling for std::chrono::hh_mm_ss - convert to string with HH:MM:SS format
270- // Must come before string_convertible to avoid conflicts
271- template<typename Duration>
272- static value convert_to_value(const std::chrono::hh_mm_ss<Duration>& time) {
273- auto ss = std::stringstream{};
274- ss << std::setw(2) << std::setfill('0') << time.hours() << ':'
275- << std::setw(2) << std::setfill('0') << time.minutes() << ':'
276- << std::setw(2) << std::setfill('0') << time.seconds();
277- return ss.str();
278- }
279-
280- // Special handling for std::error_code - convert to string with value and message
292+ // Special handling for std::error_code - provides more informative format than standard operator<<
293+ // Standard operator<< outputs: "generic:17" (category:value)
294+ // Our format outputs: "17 (File exists)" (value + human-readable message)
295+ // This is more useful for logging because it includes the error message
281296 static value convert_to_value(const std::error_code& ec) {
282297 return std::to_string(ec.value()) + " (" + ec.message() + ")";
283298 }
284299
300+ // Handles types convertible to std::string (but NOT C-strings, which are handled above)
301+ // This catches other string-like types that aren't exact matches
285302 template<string_convertible T>
286303 static value convert_to_value(T&& v) {
287- return std::string(v);
304+ return std::string{v};
305+ }
306+
307+ // Generic fallback: any type with operator<< - convert via stringstream
308+ // This handles chrono types (hh_mm_ss, day, month, year, year_month_day) and any other streamable types
309+ // ADL will find custom operator<< implementations (e.g., net::operator<< for hh_mm_ss)
310+ template<streamable T>
311+ static value convert_to_value(const T& v) {
312+ auto ss = std::stringstream{};
313+ ss << v;
314+ return ss.str();
288315 }
289316
290317 void output() {
@@ -457,17 +484,7 @@ public:
457484
458485 // Structured fields: {"key", value} or std::pair{"key", value}
459486 // Optimized: uses emplace to avoid double conversion
460- template<class K, class V>
461- structured_log_stream& operator<<(const std::pair<K, V>& kv) {
462- if (entry_enabled) {
463- structured_fields.emplace(std::piecewise_construct,
464- std::forward_as_tuple(kv.first),
465- std::forward_as_tuple(convert_to_value(kv.second)));
466- }
467- return *this;
468- }
469-
470- // Support for braced-init-list syntax: {"key", value}
487+ // Handles both lvalue and rvalue references via forwarding reference
471488 template<class K, class V>
472489 structured_log_stream& operator<<(std::pair<K, V>&& kv) {
473490 if (entry_enabled) {
0 commit comments