mirror of
https://github.com/ETLCPP/etl.git
synced 2026-07-30 16:26:17 +08:00
Format user defined types in format.h (#1470)
Co-authored-by: John Wellbelove <jwellbelove@users.noreply.github.com>
This commit is contained in:
parent
8f4373e555
commit
5e5012c62c
@ -302,6 +302,89 @@ Requires `ETL_USING_FORMAT_FLOATING_POINT`.
|
|||||||
|
|
||||||
`nan`, `inf` (lowercase for `e`/`f`/`g`/`a`, uppercase for `E`/`F`/`G`/`A`).
|
`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<T>`, mirroring the `std::formatter<T>` 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 <class T, class CharT = char>
|
||||||
|
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 <class OutputIt> format_context<OutputIt>::iterator format(const T& value, format_context<OutputIt>& 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<char>`. Provides `begin()`, `end()`, and `advance_to()` over the format-spec characters. Its `iterator` is `etl::string_view::const_iterator`. |
|
||||||
|
| `etl::format_context<OutputIt>` | Alias for `etl::basic_format_context<OutputIt, char>`. 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<coordinate>
|
||||||
|
{
|
||||||
|
format_parse_context::iterator parse(format_parse_context& parse_ctx)
|
||||||
|
{
|
||||||
|
return parse_ctx.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class OutputIt>
|
||||||
|
typename format_context<OutputIt>::iterator format(const coordinate& c, format_context<OutputIt>& 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<T>` 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<T>` exposes a callable `parse()` member, so no
|
||||||
|
opt-in trait or registration is required.
|
||||||
|
|
||||||
## Escape Sequences and Literal Braces
|
## Escape Sequences and Literal Braces
|
||||||
|
|
||||||
### 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. |
|
| **`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)`. |
|
| **`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`. |
|
| **Floating-point support** | Always available | Opt-in via `ETL_USING_FORMAT_FLOATING_POINT`. |
|
||||||
| **User-defined formatters** | `std::formatter<T>` specialisations | Not yet supported. |
|
| **User-defined formatters** | `std::formatter<T>` specialisations | Supported via `etl::formatter<T>` specialisations (see [Formatting User-Defined Types](#formatting-user-defined-types)). |
|
||||||
| **Locale** | `L` flag uses `std::locale` | `L` flag is parsed but has no effect. |
|
| **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`. |
|
| **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. |
|
| **`format_to_n` return type** | `std::format_to_n_result` | Returns the underlying `OutputIt` directly. |
|
||||||
|
|||||||
@ -513,21 +513,6 @@ namespace etl
|
|||||||
template <class... Args>
|
template <class... Args>
|
||||||
using format_string = basic_format_string<type_identity_t<Args>...>;
|
using format_string = basic_format_string<type_identity_t<Args>...>;
|
||||||
|
|
||||||
// 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 CharT>
|
template <class CharT>
|
||||||
class basic_format_parse_context
|
class basic_format_parse_context
|
||||||
{
|
{
|
||||||
@ -595,25 +580,89 @@ namespace etl
|
|||||||
|
|
||||||
using format_parse_context = basic_format_parse_context<char>;
|
using format_parse_context = basic_format_parse_context<char>;
|
||||||
|
|
||||||
|
// 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 <class T, class CharT = char>
|
||||||
|
struct formatter;
|
||||||
|
|
||||||
|
// Forward declaration of the format context (defined later) so that
|
||||||
|
// is_formattable can probe for a usable formatter<T>::format() member, which
|
||||||
|
// is templated on the output iterator type.
|
||||||
|
template <class OutputIt, class CharT>
|
||||||
|
class basic_format_context;
|
||||||
|
|
||||||
|
namespace private_format
|
||||||
|
{
|
||||||
|
// Detects whether etl::formatter<T> exposes a usable parse() member.
|
||||||
|
template <typename T, typename = void>
|
||||||
|
struct has_formatter_parse : etl::false_type
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct has_formatter_parse<T, etl::void_t<decltype(etl::declval<etl::formatter<T>&>().parse(etl::declval<format_parse_context&>()))> >
|
||||||
|
: etl::true_type
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
// Detects whether etl::formatter<T> exposes a usable format() member.
|
||||||
|
// format() is templated on the output iterator, so it is probed with a
|
||||||
|
// representative char* output iterator context.
|
||||||
|
template <typename T, typename = void>
|
||||||
|
struct has_formatter_format : etl::false_type
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct has_formatter_format<T, etl::void_t<decltype(etl::declval<etl::formatter<T>&>().format(
|
||||||
|
etl::declval<const T&>(), etl::declval<etl::basic_format_context<char*, char>&>()))> > : 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 <typename T>
|
||||||
|
struct is_formattable : etl::bool_constant<has_formatter_parse<T>::value && has_formatter_format<T>::value>
|
||||||
|
{
|
||||||
|
};
|
||||||
|
} // namespace private_format
|
||||||
|
|
||||||
template <class Context>
|
template <class Context>
|
||||||
class basic_format_arg
|
class basic_format_arg
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
// Type-erased wrapper that allows user-defined types to be formatted via an
|
||||||
|
// etl::formatter<T> specialisation, mirroring std::basic_format_arg::handle.
|
||||||
class handle
|
class handle
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
||||||
void format(etl::basic_format_parse_context<char>& /* parse_ctx */, Context& /*format_ctx*/)
|
template <typename T>
|
||||||
|
explicit handle(const T& value)
|
||||||
|
: obj(static_cast<const void*>(etl::addressof(value)))
|
||||||
|
, func(&format_custom_type<T>)
|
||||||
{
|
{
|
||||||
// typename Context::template formatter_type<TD> f;
|
}
|
||||||
// parse_ctx.advance_to(f.parse(parse_ctx));
|
|
||||||
// format_ctx.advance_to(f.format(const_cast<TQ&>(static_cast<const
|
void format(etl::basic_format_parse_context<char>& parse_ctx, Context& format_ctx) const
|
||||||
// TD&>(ref)), format_ctx));
|
{
|
||||||
|
func(parse_ctx, format_ctx, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static void format_custom_type(etl::basic_format_parse_context<char>& parse_ctx, Context& format_ctx, const void* ptr)
|
||||||
|
{
|
||||||
|
const T& value = *static_cast<const T*>(ptr);
|
||||||
|
etl::formatter<T> f;
|
||||||
|
parse_ctx.advance_to(f.parse(parse_ctx));
|
||||||
|
format_ctx.advance_to(f.format(value, format_ctx));
|
||||||
|
}
|
||||||
|
|
||||||
const void* obj;
|
const void* obj;
|
||||||
typedef void (*function_type)(etl::basic_format_parse_context<char>&, Context&, const void*);
|
typedef void (*function_type)(etl::basic_format_parse_context<char>&, Context&, const void*);
|
||||||
function_type func;
|
function_type func;
|
||||||
@ -725,6 +774,15 @@ namespace etl
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converting constructor for user-defined types that provide an
|
||||||
|
// etl::formatter<T> specialisation. The value is stored type-erased in a
|
||||||
|
// handle, matching the std::basic_format_arg behaviour for custom types.
|
||||||
|
template <typename T, typename = etl::enable_if_t<private_format::is_formattable<T>::value> >
|
||||||
|
basic_format_arg(const T& v)
|
||||||
|
: data(handle(v))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
basic_format_arg& operator=(const basic_format_arg& other)
|
basic_format_arg& operator=(const basic_format_arg& other)
|
||||||
{
|
{
|
||||||
data = other.data;
|
data = other.data;
|
||||||
@ -744,7 +802,21 @@ namespace etl
|
|||||||
|
|
||||||
private:
|
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 <class Context, class... Args>
|
template <class Context, class... Args>
|
||||||
@ -1157,7 +1229,7 @@ namespace etl
|
|||||||
}
|
}
|
||||||
} // namespace private_format
|
} // namespace private_format
|
||||||
|
|
||||||
template <class T, class CharT = char>
|
template <class T, class CharT>
|
||||||
struct formatter
|
struct formatter
|
||||||
{
|
{
|
||||||
using char_type = CharT;
|
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 <typename T>
|
template <typename T>
|
||||||
void operator()(T value)
|
void operator()(T value)
|
||||||
{
|
{
|
||||||
@ -1917,6 +1989,12 @@ namespace etl
|
|||||||
fmt_ctx.advance_to(fit);
|
fmt_ctx.advance_to(fit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for user-defined types routed through basic_format_arg::handle
|
||||||
|
void operator()(typename basic_format_arg<format_context<OutputIt> >::handle h)
|
||||||
|
{
|
||||||
|
h.format(parse_ctx, fmt_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
format_parse_context& parse_ctx;
|
format_parse_context& parse_ctx;
|
||||||
format_context<OutputIt>& fmt_ctx;
|
format_context<OutputIt>& fmt_ctx;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -34,6 +34,38 @@ SOFTWARE.
|
|||||||
|
|
||||||
#if ETL_USING_CPP11
|
#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<coordinate>
|
||||||
|
{
|
||||||
|
format_parse_context::iterator parse(format_parse_context& parse_ctx)
|
||||||
|
{
|
||||||
|
return parse_ctx.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class OutputIt>
|
||||||
|
typename format_context<OutputIt>::iterator format(const coordinate& c, format_context<OutputIt>& fmt_ctx)
|
||||||
|
{
|
||||||
|
return etl::format_to(fmt_ctx.out(), "({}, {})", c.x, c.y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace etl
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
using iterator = etl::back_insert_iterator<etl::istring>;
|
using iterator = etl::back_insert_iterator<etl::istring>;
|
||||||
@ -928,6 +960,36 @@ namespace
|
|||||||
CHECK_EQUAL("07", test_format(s, "{:#o}", 7));
|
CHECK_EQUAL("07", test_format(s, "{:#o}", 7));
|
||||||
CHECK_EQUAL("010", test_format(s, "{:#o}", 8));
|
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
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user