diff --git a/docs/strings/format.md b/docs/strings/format.md index 4d9bedca..7691b478 100644 --- a/docs/strings/format.md +++ b/docs/strings/format.md @@ -302,6 +302,89 @@ Requires `ETL_USING_FORMAT_FLOATING_POINT`. `nan`, `inf` (lowercase for `e`/`f`/`g`/`a`, uppercase for `E`/`F`/`G`/`A`). +## Formatting User-Defined Types + +Custom types can be made formattable by providing a specialisation of +`etl::formatter`, mirroring the `std::formatter` mechanism. Once a +specialisation is in scope, values of that type can be passed to `format_to`, +`format_to_n`, `formatted_size`, `print`, and `println` exactly like the +built-in types. + +### The `etl::formatter` primary template + +```cpp +template +struct formatter; +``` + +The primary template is empty. A type `T` becomes formattable when you provide +a specialisation (in namespace `etl`) that defines two member functions: + +| Member | Signature | Purpose | +|---|---|---| +| `parse` | `format_parse_context::iterator parse(format_parse_context& parse_ctx)` | Parses the format-spec portion of the replacement field (the characters after `:`, up to the closing `}`). Returns an iterator to the first unconsumed character, which must be the closing `}`. Return `parse_ctx.begin()` to consume nothing. | +| `format` | `template format_context::iterator format(const T& value, format_context& fmt_ctx)` | Writes the formatted representation of `value` through `fmt_ctx.out()` and returns the iterator past the last character written. | + +### Context types + +| Type | Description | +|---|---| +| `etl::format_parse_context` | Alias for `etl::basic_format_parse_context`. Provides `begin()`, `end()`, and `advance_to()` over the format-spec characters. Its `iterator` is `etl::string_view::const_iterator`. | +| `etl::format_context` | Alias for `etl::basic_format_context`. Provides `out()` (the current output iterator) and `advance_to()`. Its `iterator` is `OutputIt`. | + +### Example + +```cpp +// A simple user-defined type. +struct coordinate +{ + int x; + int y; +}; + +// Specialise etl::formatter for it. Here format() simply delegates to +// etl::format_to, writing the two members as "(x, y)". +namespace etl +{ + template <> + struct formatter + { + format_parse_context::iterator parse(format_parse_context& parse_ctx) + { + return parse_ctx.begin(); + } + + template + typename format_context::iterator format(const coordinate& c, format_context& fmt_ctx) + { + return etl::format_to(fmt_ctx.out(), "({}, {})", c.x, c.y); + } + }; +} +``` + +With the specialisation in scope, `coordinate` behaves like any other argument: + +```cpp +etl::string<100> s; +coordinate c{3, 7}; + +etl::format_to(s, "{}", c); // "(3, 7)" +etl::format_to(s, "point={}", c); // "point=(3, 7)" +etl::format_to(s, "{0} and {0}", c); // "(3, 7) and (3, 7)" +etl::format_to(s, "{} {} {}", 'a', c, 5); // "a (3, 7) 5" +etl::formatted_size("{}", c); // 6 +``` + +### How it works + +When an argument type provides an `etl::formatter` specialisation, ETL stores +the value type-erased inside `basic_format_arg::handle` rather than in one of the +built-in alternatives. During formatting the handle invokes the formatter's +`parse` and `format` members. Detection is automatic: a type is treated as a +custom type when `etl::formatter` exposes a callable `parse()` member, so no +opt-in trait or registration is required. + ## Escape Sequences and Literal Braces ### Literal braces @@ -378,7 +461,7 @@ etl::format_to(s, "{:d}", sv); // invalid type for string_view | **`etl::istring&` overload** | Not available | `format_to(etl::istring&, ...)` automatically resizes the string. | | **`print` / `println` output** | Writes to `FILE*` / `stdout` | Writes character-by-character via user-defined `etl_putchar(int)`. | | **Floating-point support** | Always available | Opt-in via `ETL_USING_FORMAT_FLOATING_POINT`. | -| **User-defined formatters** | `std::formatter` specialisations | Not yet supported. | +| **User-defined formatters** | `std::formatter` specialisations | Supported via `etl::formatter` specialisations (see [Formatting User-Defined Types](#formatting-user-defined-types)). | | **Locale** | `L` flag uses `std::locale` | `L` flag is parsed but has no effect. | | **Compile-time validation** | Enforced via `consteval` on C++20 | Planned; currently validates at run time and throws `etl::bad_format_string_exception`. | | **`format_to_n` return type** | `std::format_to_n_result` | Returns the underlying `OutputIt` directly. | diff --git a/include/etl/format.h b/include/etl/format.h index e2142cec..75ac5952 100644 --- a/include/etl/format.h +++ b/include/etl/format.h @@ -513,21 +513,6 @@ namespace etl template using format_string = basic_format_string...>; - // Supported types to format - // - // This is the limited number of types as defined in std::basic_format_arg - // https://en.cppreference.com/w/cpp/utility/format/basic_format_arg.html - // - // Further types to be supported are added via converting constructors in - // etl::basic_format_arg - using supported_format_types = etl::variant< etl::monostate, bool, char, int, unsigned int, long long int, unsigned long long int, - #if ETL_USING_FORMAT_FLOATING_POINT - float, double, long double, - #endif - const char*, etl::string_view, const void* - // basic_format_arg::handle, - >; - template class basic_format_parse_context { @@ -595,25 +580,89 @@ namespace etl using format_parse_context = basic_format_parse_context; + // Forward declaration of the formatter primary template (defined later). This + // lets basic_format_arg detect user-provided formatter specialisations for + // custom types and route them through basic_format_arg::handle. + template + struct formatter; + + // Forward declaration of the format context (defined later) so that + // is_formattable can probe for a usable formatter::format() member, which + // is templated on the output iterator type. + template + class basic_format_context; + + namespace private_format + { + // Detects whether etl::formatter exposes a usable parse() member. + template + struct has_formatter_parse : etl::false_type + { + }; + + template + struct has_formatter_parse&>().parse(etl::declval()))> > + : etl::true_type + { + }; + + // Detects whether etl::formatter exposes a usable format() member. + // format() is templated on the output iterator, so it is probed with a + // representative char* output iterator context. + template + struct has_formatter_format : etl::false_type + { + }; + + template + struct has_formatter_format&>().format( + etl::declval(), etl::declval&>()))> > : etl::true_type + { + }; + + // The primary formatter template is empty, so a type is only formattable + // when a (user-)provided specialisation supplies both a usable parse() and + // a usable format() member - mirroring the std::formattable requirements. + template + struct is_formattable : etl::bool_constant::value && has_formatter_format::value> + { + }; + } // namespace private_format + template class basic_format_arg { public: + // Type-erased wrapper that allows user-defined types to be formatted via an + // etl::formatter specialisation, mirroring std::basic_format_arg::handle. class handle { public: - void format(etl::basic_format_parse_context& /* parse_ctx */, Context& /*format_ctx*/) + template + explicit handle(const T& value) + : obj(static_cast(etl::addressof(value))) + , func(&format_custom_type) { - // typename Context::template formatter_type f; - // parse_ctx.advance_to(f.parse(parse_ctx)); - // format_ctx.advance_to(f.format(const_cast(static_cast(ref)), format_ctx)); + } + + void format(etl::basic_format_parse_context& parse_ctx, Context& format_ctx) const + { + func(parse_ctx, format_ctx, obj); } private: + template + static void format_custom_type(etl::basic_format_parse_context& parse_ctx, Context& format_ctx, const void* ptr) + { + const T& value = *static_cast(ptr); + etl::formatter f; + parse_ctx.advance_to(f.parse(parse_ctx)); + format_ctx.advance_to(f.format(value, format_ctx)); + } + const void* obj; typedef void (*function_type)(etl::basic_format_parse_context&, Context&, const void*); function_type func; @@ -725,6 +774,15 @@ namespace etl { } + // Converting constructor for user-defined types that provide an + // etl::formatter specialisation. The value is stored type-erased in a + // handle, matching the std::basic_format_arg behaviour for custom types. + template ::value> > + basic_format_arg(const T& v) + : data(handle(v)) + { + } + basic_format_arg& operator=(const basic_format_arg& other) { data = other.data; @@ -744,7 +802,21 @@ namespace etl private: - supported_format_types data; + // Storage for the argument value. + // + // This is the limited number of types as defined in std::basic_format_arg + // https://en.cppreference.com/w/cpp/utility/format/basic_format_arg.html + // + // Further types to be supported are mapped onto these via the converting + // constructors above. User-defined types are stored type-erased in the + // special handle member, which formats them through their etl::formatter + // specialisation. + etl::variant< etl::monostate, bool, char, int, unsigned int, long long int, unsigned long long int, + #if ETL_USING_FORMAT_FLOATING_POINT + float, double, long double, + #endif + const char*, etl::string_view, const void*, handle > + data; }; template @@ -1157,7 +1229,7 @@ namespace etl } } // namespace private_format - template + template struct formatter { using char_type = CharT; @@ -1906,7 +1978,7 @@ namespace etl { } - // for all types in supported_format_types + // for all the built-in alternatives stored in basic_format_arg template void operator()(T value) { @@ -1917,6 +1989,12 @@ namespace etl fmt_ctx.advance_to(fit); } + // for user-defined types routed through basic_format_arg::handle + void operator()(typename basic_format_arg >::handle h) + { + h.format(parse_ctx, fmt_ctx); + } + format_parse_context& parse_ctx; format_context& fmt_ctx; }; diff --git a/test/test_format.cpp b/test/test_format.cpp index 2397955f..6fd2cbf4 100644 --- a/test/test_format.cpp +++ b/test/test_format.cpp @@ -34,6 +34,38 @@ SOFTWARE. #if ETL_USING_CPP11 +namespace +{ + // A simple user-defined type with two members, used to demonstrate the + // formatting of custom types through an etl::formatter specialisation. + struct coordinate + { + int x; + int y; + }; +} // namespace + +// Custom formatter specialisation for the user-defined 'coordinate' type. +// Like a std::formatter, it provides parse() and format(); here format() +// simply delegates to etl::format_to, writing the two members as "(x, y)". +namespace etl +{ + template <> + struct formatter + { + format_parse_context::iterator parse(format_parse_context& parse_ctx) + { + return parse_ctx.begin(); + } + + template + typename format_context::iterator format(const coordinate& c, format_context& fmt_ctx) + { + return etl::format_to(fmt_ctx.out(), "({}, {})", c.x, c.y); + } + }; +} // namespace etl + namespace { using iterator = etl::back_insert_iterator; @@ -928,6 +960,36 @@ namespace CHECK_EQUAL("07", test_format(s, "{:#o}", 7)); CHECK_EQUAL("010", test_format(s, "{:#o}", 8)); } + + //************************************************************************* + TEST(test_format_custom_type) + { + etl::string<100> s; + + coordinate c{3, 7}; + + // Default presentation of a custom type. + CHECK_EQUAL("(3, 7)", test_format(s, "{}", c)); + + // Surrounded by literal text. + CHECK_EQUAL("point=(3, 7)", test_format(s, "point={}", c)); + + // Manual argument index, referenced more than once. + CHECK_EQUAL("(3, 7) and (3, 7)", test_format(s, "{0} and {0}", c)); + + // Mixed in with built-in argument types. + CHECK_EQUAL("a (3, 7) 5", test_format(s, "{} {} {}", 'a', c, 5)); + + // Negative member values. + coordinate n{-1, 42}; + CHECK_EQUAL("(-1, 42)", test_format(s, "{}", n)); + + // A temporary custom-type argument. + CHECK_EQUAL("(8, 9)", test_format(s, "{}", coordinate{8, 9})); + + // formatted_size also works with custom types. + CHECK_EQUAL(6, etl::formatted_size("{}", c)); + } } } // namespace