Skip to content

Commit fa61df0

Browse files
committed
Switch per-field chrono wrappers to value-based call syntax
date_format and epoch_count now take the member pointer as a value argument: glz::date_format(&T::start, "%Y-%m-%d %H:%M:%S") glz::epoch_count<std::chrono::milliseconds>(&T::logged) Previously the member pointer and (for date_format) the format string were non-type template parameters. Carrying the format as a runtime std::string_view keys the view type on the member type alone, so fields that differ only in their format string no longer each instantiate a fresh date_format_t / to / from / closure. On a synthetic struct of N same-typed members with distinct formats this removed ~40% of the wrapper's template instantiations and made the per-distinct- format compile cost ~flat (it tracks a single shared format). date_format's strftime walk was already runtime, so codegen is unchanged. The strftime pattern stays a compile-time literal: a consteval guard in the date_format() factory still rejects unsupported tokens, a missing calendar date, and time tokens on a year_month_day field. float_format keeps its NTTP form because it relies on std::format's compile-time format checking. The reader keeps the seconds-anchored reconstruction (sys_seconds) so far-future years stay exact under MSVC's 32-bit chrono hours/minutes rep.
1 parent bf26665 commit fa61df0

4 files changed

Lines changed: 128 additions & 87 deletions

File tree

docs/chrono.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ The clock type alone decides the default representation (ISO 8601 for `system_cl
186186
187187
### `glz::date_format` — custom strftime-subset pattern
188188
189-
`glz::date_format<&T::member, "pattern">` serializes a `system_clock` time point (or a `year_month_day`) using a `strftime`-style pattern instead of ISO 8601:
189+
`glz::date_format(&T::member, "pattern")` serializes a `system_clock` time point (or a `year_month_day`) using a `strftime`-style pattern instead of ISO 8601:
190190
191191
```cpp
192192
#include "glaze/chrono.hpp"
@@ -200,11 +200,13 @@ template <>
200200
struct glz::meta<Event> {
201201
using T = Event;
202202
static constexpr auto value = glz::object(
203-
"start", glz::date_format<&T::start, "%Y-%m-%d %H:%M:%S">, // "2026-06-18 12:34:56"
204-
"day", glz::date_format<&T::day, "%Y/%m/%d">); // "2026/06/18"
203+
"start", glz::date_format(&T::start, "%Y-%m-%d %H:%M:%S"), // "2026-06-18 12:34:56"
204+
"day", glz::date_format(&T::day, "%Y/%m/%d")); // "2026/06/18"
205205
};
206206
```
207207

208+
The member pointer and pattern are passed as ordinary arguments (not template parameters), so fields that share a member type but differ in format do not each spin up a fresh set of template instantiations. The pattern is still a compile-time literal and is validated at compile time.
209+
208210
Supported conversion specifiers (locale-independent by design):
209211

210212
| Token | Meaning | Token | Meaning |
@@ -229,7 +231,7 @@ Notes and limitations (MVP scope):
229231

230232
### `glz::epoch_count` — per-field Unix timestamp
231233

232-
`glz::epoch_count<&T::member, Duration>` serializes a `system_clock` time point as a numeric Unix timestamp in units of `Duration`. It is the per-field counterpart to the `glz::epoch_time` storage wrapper, letting one field be an epoch count while others stay ISO 8601:
234+
`glz::epoch_count<Duration>(&T::member)` serializes a `system_clock` time point as a numeric Unix timestamp in units of `Duration`. It is the per-field counterpart to the `glz::epoch_time` storage wrapper, letting one field be an epoch count while others stay ISO 8601:
233235

234236
```cpp
235237
#include "glaze/chrono.hpp"
@@ -244,7 +246,7 @@ struct glz::meta<Reading> {
244246
using T = Reading;
245247
static constexpr auto value = glz::object(
246248
"observed", &T::observed, // "2026-06-18T12:34:56Z"
247-
"logged", glz::epoch_count<&T::logged, std::chrono::milliseconds>); // 1781786096789
249+
"logged", glz::epoch_count<std::chrono::milliseconds>(&T::logged)); // 1781786096789
248250
};
249251
```
250252

include/glaze/core/format_str.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace glz
1212
// Compile-time string for use as a non-type template parameter.
1313
// Captures a string literal (including its null terminator) so that format
1414
// specifications can be carried through the type system, e.g.
15-
// glz::float_format<&T::x, "{:.2f}"> or glz::date_format<&T::t, "%Y-%m-%d">.
15+
// glz::float_format<&T::x, "{:.2f}">.
1616
template <size_t N>
1717
struct format_str
1818
{

include/glaze/json/chrono_format.hpp

Lines changed: 113 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,32 @@
88
#include <type_traits>
99

1010
#include "glaze/core/chrono.hpp"
11-
#include "glaze/core/format_str.hpp"
1211
#include "glaze/json/read.hpp"
1312
#include "glaze/json/write.hpp"
1413

1514
// Per-field chrono customization wrappers.
1615
//
1716
// These decouple the *format* from the chrono *type*, addressing the limitation
18-
// that the clock type alone dictates the wire representation. They follow the
19-
// established field-wrapper precedents (glz::float_format for the NTTP format
20-
// string, glz::quoted for the fully-custom read+write pair) and are applied
17+
// that the clock type alone dictates the wire representation. They are applied
2118
// inside glz::object(...) within a glz::meta<T> specialization:
2219
//
2320
// template <> struct glz::meta<event> {
2421
// using T = event;
2522
// static constexpr auto value = glz::object(
26-
// "start", glz::date_format<&T::start, "%Y-%m-%d %H:%M:%S">,
27-
// "logged", glz::epoch_count<&T::logged, std::chrono::milliseconds>);
23+
// "start", glz::date_format(&T::start, "%Y-%m-%d %H:%M:%S"),
24+
// "logged", glz::epoch_count<std::chrono::milliseconds>(&T::logged));
2825
// };
2926
//
27+
// Both take the member pointer as a value argument. The format string (date_format)
28+
// and member pointer live as data rather than as non-type template parameters, so a
29+
// struct with many distinct formats does not multiply the wrapper's template
30+
// instantiations: the view type is keyed on the member type alone, and the format is
31+
// carried as a runtime std::string_view. The strftime pattern is still validated at
32+
// compile time — a consteval guard in the date_format() factory rejects unsupported
33+
// tokens, a missing calendar date, or time tokens on a year_month_day field. (glz::
34+
// float_format keeps its NTTP form because it leans on std::format's compile-time
35+
// format checking, which date_format's hand-rolled runtime walk does not need.)
36+
//
3037
// Scope (MVP): JSON only. date_format supports system_clock time_points and
3138
// year_month_day; epoch_count supports system_clock time_points. utc_time and
3239
// the binary backends are intentionally out of scope here.
@@ -37,64 +44,36 @@ namespace glz
3744
// glz::date_format — per-field strftime-subset format string
3845
// ============================================
3946

40-
// Wrapper carrying the compile-time format string and a reference to the field.
41-
// Both directions are fully custom (no meta<>::value indirection) because the
42-
// underlying chrono parser only understands ISO 8601 and cannot honor Fmt.
47+
// View carrying the bound member and the (runtime) format string. Parameterized on the
48+
// member type alone, so two fields that differ only in their format string share one set
49+
// of to<>/from<> instantiations.
4350
//
4451
// glaze_reflect = false keeps the binary backends from silently reflecting the
4552
// wrapper as an aggregate: date_format is a textual, JSON-only wrapper, so a
4653
// binary serialization should fail loudly (undefined to<BEVE, ...>) rather than
4754
// emit a meaningless encoding of the wrapper struct.
48-
template <format_str Fmt, class T>
55+
template <class T>
4956
struct date_format_t
5057
{
5158
static constexpr bool glaze_wrapper = true;
5259
static constexpr bool glaze_reflect = false;
5360
using value_type = T;
5461
T& val;
62+
std::string_view fmt;
5563
};
5664

57-
namespace chrono_detail
58-
{
59-
// Compile-time guards shared by the date_format read and write paths.
60-
template <format_str Fmt, class V>
61-
consteval void validate_date_format() noexcept
62-
{
63-
static_assert(is_system_time_point<V> || is_year_month_day<V>,
64-
"glz::date_format requires a std::chrono::system_clock time_point or year_month_day member");
65-
static_assert(date_format_tokens_valid(std::string_view(Fmt)),
66-
"glz::date_format: unsupported format token (supported: %Y %m %d %H %M %S %F %T %%)");
67-
static_assert(date_format_has_full_date(std::string_view(Fmt)),
68-
"glz::date_format: format must include a full date (%Y %m %d, or %F)");
69-
if constexpr (is_year_month_day<V>) {
70-
static_assert(!date_format_has_time(std::string_view(Fmt)),
71-
"glz::date_format: a year_month_day field must not include time tokens (%H %M %S %T)");
72-
}
73-
}
74-
75-
// Compile-time guards shared by the epoch_count read and write paths. Validates both
76-
// axes (the member type and the Duration unit) so a misuse on either surfaces as a
77-
// friendly message rather than a deep cascade out of `typename Duration::rep`.
78-
template <class Duration, class V>
79-
consteval void validate_epoch_count() noexcept
80-
{
81-
static_assert(is_system_time_point<V>,
82-
"glz::epoch_count requires a std::chrono::system_clock time_point member");
83-
static_assert(is_duration<Duration>,
84-
"glz::epoch_count requires a std::chrono::duration unit (e.g. std::chrono::milliseconds)");
85-
}
86-
}
87-
88-
template <format_str Fmt, class T>
89-
struct to<JSON, date_format_t<Fmt, T>>
65+
template <class T>
66+
struct to<JSON, date_format_t<T>>
9067
{
9168
template <auto Opts, class B>
9269
static void op(auto&& wrapper, is_context auto&& ctx, B&& b, auto& ix) noexcept
9370
{
9471
using namespace std::chrono;
9572
using V = std::remove_cvref_t<T>;
96-
chrono_detail::validate_date_format<Fmt, V>();
73+
static_assert(is_system_time_point<V> || is_year_month_day<V>,
74+
"glz::date_format requires a std::chrono::system_clock time_point or year_month_day member");
9775

76+
const std::string_view fmt = wrapper.fmt;
9877
chrono_detail::date_time_fields f{};
9978

10079
if constexpr (is_year_month_day<V>) {
@@ -131,12 +110,11 @@ namespace glz
131110
}
132111

133112
// Upper bound on rendered size: the widest token is %F, which expands two source
134-
// characters to "YYYY-MM-DD" (10 bytes, 5 per source char); %T expands two to
135-
// "HH:MM:SS" (8 bytes, 4 per source char). *6 bounds both with room to spare, plus a
136-
// small constant for the surrounding quotes. Skipped on the write_unchecked fast path,
137-
// where the caller has already reserved the space (matches to<JSON, num_t>).
113+
// characters to "YYYY-MM-DD" (10 bytes, 5 per source char). *6 bounds it with room
114+
// to spare, plus a small constant for the surrounding quotes. Skipped on the
115+
// write_unchecked fast path, where the caller has already reserved the space.
138116
if constexpr (not check_write_unchecked(Opts)) {
139-
const size_t max_size = std::string_view(Fmt).size() * 6 + 4;
117+
const size_t max_size = fmt.size() * 6 + 4;
140118
if (!ensure_space(ctx, b, ix + max_size)) [[unlikely]] {
141119
return;
142120
}
@@ -145,30 +123,31 @@ namespace glz
145123
if constexpr (not check_unquoted(Opts)) {
146124
b[ix++] = '"';
147125
}
148-
chrono_detail::write_date_format(std::string_view(Fmt), f, b, ix);
126+
chrono_detail::write_date_format(fmt, f, b, ix);
149127
if constexpr (not check_unquoted(Opts)) {
150128
b[ix++] = '"';
151129
}
152130
}
153131
};
154132

155-
template <format_str Fmt, class T>
156-
struct from<JSON, date_format_t<Fmt, T>>
133+
template <class T>
134+
struct from<JSON, date_format_t<T>>
157135
{
158136
template <auto Opts>
159137
static void op(auto&& wrapper, is_context auto&& ctx, auto&&... args) noexcept
160138
{
161139
using namespace std::chrono;
162140
using V = std::remove_cvref_t<T>;
163-
chrono_detail::validate_date_format<Fmt, V>();
141+
static_assert(is_system_time_point<V> || is_year_month_day<V>,
142+
"glz::date_format requires a std::chrono::system_clock time_point or year_month_day member");
164143

165144
std::string_view str;
166145
from<JSON, std::string_view>::template op<Opts>(str, ctx, args...);
167146
if (bool(ctx.error)) [[unlikely]]
168147
return;
169148

170149
chrono_detail::date_time_fields f{};
171-
chrono_detail::parse_date_format(std::string_view(Fmt), str, f, ctx.error);
150+
chrono_detail::parse_date_format(wrapper.fmt, str, f, ctx.error);
172151
if (bool(ctx.error)) [[unlikely]]
173152
return;
174153

@@ -196,23 +175,65 @@ namespace glz
196175
}
197176
};
198177

199-
template <auto MemPtr, format_str Fmt>
200-
inline constexpr decltype(auto) date_format_impl() noexcept
178+
namespace chrono_detail
201179
{
202-
return [](auto&& val) { return date_format_t<Fmt, std::remove_reference_t<decltype(val.*MemPtr)>>{val.*MemPtr}; };
180+
// Compile-time validation for date_format(). Rejects unsupported tokens, a missing
181+
// calendar date, and time tokens applied to a year_month_day field. Runs inside the
182+
// consteval date_format() factory, where the literal is a constant expression, even
183+
// though the format is carried onward as a runtime value (a throw on the error path
184+
// makes the surrounding immediate invocation non-constant, i.e. a hard compile error).
185+
template <class Mem>
186+
consteval void validate_date_format(std::string_view fmt)
187+
{
188+
static_assert(is_system_time_point<Mem> || is_year_month_day<Mem>,
189+
"glz::date_format requires a std::chrono::system_clock time_point or year_month_day member");
190+
if (!date_format_tokens_valid(fmt)) {
191+
throw "glz::date_format: unsupported format token (supported: %Y %m %d %H %M %S %F %T %%)";
192+
}
193+
if (!date_format_has_full_date(fmt)) {
194+
throw "glz::date_format: format must include a full date (%Y %m %d, or %F)";
195+
}
196+
if constexpr (is_year_month_day<Mem>) {
197+
if (date_format_has_time(fmt)) {
198+
throw "glz::date_format: a year_month_day field must not include time tokens (%H %M %S %T)";
199+
}
200+
}
201+
}
203202
}
204203

205-
// Usage: glz::date_format<&T::member, "%Y-%m-%d %H:%M:%S">
206-
template <auto MemPtr, format_str Fmt>
207-
constexpr auto date_format = date_format_impl<MemPtr, Fmt>();
204+
// The object element: an invocable value carrying the member pointer + format. When the
205+
// serializer iterates the meta object it calls this with the live instance (the same
206+
// std::invocable dispatch every glaze field wrapper uses) to bind the member by reference.
207+
template <class MemPtr>
208+
struct date_format_spec
209+
{
210+
MemPtr ptr;
211+
std::string_view fmt;
212+
213+
constexpr auto operator()(auto&& parent) const
214+
{
215+
using Mem = std::remove_reference_t<decltype(parent.*ptr)>;
216+
return date_format_t<Mem>{parent.*ptr, fmt};
217+
}
218+
};
219+
220+
// Usage: glz::date_format(&T::member, "%Y-%m-%d %H:%M:%S")
221+
template <class MemPtr>
222+
consteval auto date_format(MemPtr ptr, const char* fmt)
223+
{
224+
using Mem = std::remove_cvref_t<typename unwrap_pointer<MemPtr>::type>;
225+
const std::string_view sv{fmt};
226+
chrono_detail::validate_date_format<Mem>(sv);
227+
return date_format_spec<MemPtr>{ptr, sv};
228+
}
208229

209230
// ============================================
210231
// glz::epoch_count — per-field Unix timestamp count
211232
// ============================================
212233

213-
// Serializes a system_clock time_point field as a numeric Unix timestamp in
214-
// units of Duration, the per-field counterpart to the glz::epoch_time storage
215-
// wrapper (so one field can be an epoch count while others stay ISO 8601).
234+
// View serializing a system_clock time_point field as a numeric Unix timestamp in units
235+
// of Duration, the per-field counterpart to the glz::epoch_time storage wrapper (so one
236+
// field can be an epoch count while others stay ISO 8601).
216237
template <class Duration, class T>
217238
struct epoch_count_t
218239
{
@@ -229,7 +250,10 @@ namespace glz
229250
static void op(auto&& wrapper, is_context auto&& ctx, B&& b, auto& ix) noexcept
230251
{
231252
using V = std::remove_cvref_t<T>;
232-
chrono_detail::validate_epoch_count<Duration, V>();
253+
static_assert(is_system_time_point<V>,
254+
"glz::epoch_count requires a std::chrono::system_clock time_point member");
255+
static_assert(is_duration<Duration>,
256+
"glz::epoch_count requires a std::chrono::duration unit (e.g. std::chrono::milliseconds)");
233257
using Rep = typename Duration::rep;
234258
const auto count = std::chrono::duration_cast<Duration>(wrapper.val.time_since_epoch()).count();
235259
to<JSON, Rep>::template op<Opts>(count, ctx, b, ix);
@@ -243,7 +267,10 @@ namespace glz
243267
static void op(auto&& wrapper, is_context auto&& ctx, auto&&... args) noexcept
244268
{
245269
using V = std::remove_cvref_t<T>;
246-
chrono_detail::validate_epoch_count<Duration, V>();
270+
static_assert(is_system_time_point<V>,
271+
"glz::epoch_count requires a std::chrono::system_clock time_point member");
272+
static_assert(is_duration<Duration>,
273+
"glz::epoch_count requires a std::chrono::duration unit (e.g. std::chrono::milliseconds)");
247274
using Rep = typename Duration::rep;
248275
Rep count{};
249276
from<JSON, Rep>::template op<Opts>(count, ctx, args...);
@@ -254,15 +281,27 @@ namespace glz
254281
}
255282
};
256283

257-
template <auto MemPtr, class Duration>
258-
inline constexpr decltype(auto) epoch_count_impl() noexcept
284+
template <class Duration, class MemPtr>
285+
struct epoch_count_spec
259286
{
260-
return [](auto&& val) {
261-
return epoch_count_t<Duration, std::remove_reference_t<decltype(val.*MemPtr)>>{val.*MemPtr};
262-
};
263-
}
287+
MemPtr ptr;
264288

265-
// Usage: glz::epoch_count<&T::member, std::chrono::milliseconds>
266-
template <auto MemPtr, class Duration>
267-
constexpr auto epoch_count = epoch_count_impl<MemPtr, Duration>();
289+
constexpr auto operator()(auto&& parent) const
290+
{
291+
using Mem = std::remove_reference_t<decltype(parent.*ptr)>;
292+
return epoch_count_t<Duration, Mem>{parent.*ptr};
293+
}
294+
};
295+
296+
// Usage: glz::epoch_count<std::chrono::milliseconds>(&T::member)
297+
template <class Duration, class MemPtr>
298+
constexpr auto epoch_count(MemPtr ptr)
299+
{
300+
using Mem = std::remove_cvref_t<typename unwrap_pointer<MemPtr>::type>;
301+
static_assert(is_system_time_point<Mem>,
302+
"glz::epoch_count requires a std::chrono::system_clock time_point member");
303+
static_assert(is_duration<Duration>,
304+
"glz::epoch_count requires a std::chrono::duration unit (e.g. std::chrono::milliseconds)");
305+
return epoch_count_spec<Duration, MemPtr>{ptr};
306+
}
268307
}

0 commit comments

Comments
 (0)