Optimize format.h (#1588)

Dispatch only the argument types which are actually stated. For everything
else, don't generate the code anymore.

Detemplatize parse_format_spec().

Collapse long double and double to double on targets where they are
same size. This helps prevent duplicate code generation.
This commit is contained in:
Roland Reichwein 2026-09-05 09:08:56 +02:00 committed by GitHub
parent 155df46889
commit c8f610a69d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -652,6 +652,197 @@ namespace etl
struct is_formattable : etl::bool_constant<has_formatter_parse<T>::value && has_formatter_format<T>::value>
{
};
//*************************************************************************
// Compile-time argument mask.
//
// basic_format_arg stores its value in a variant with a fixed set of
// alternatives, and etl::visit dispatches over every one of them. That
// would instantiate etl::formatter for all alternatives - including the
// whole floating point formatting chain - even for a call that only ever
// passes an int.
//
// The mask records which alternatives can actually occur for a given pack
// of argument types. format_visitor turns the unreachable alternatives into
// empty functions, so their formatters are never instantiated and the
// linker never sees the code.
//
// A single table, arg_type_mask, drives both halves: it builds the mask
// from the argument pack, and it tells the visitor which bit an alternative
// corresponds to.
//
// Soundness: the mask is only ever narrowed for argument types that are
// recognised explicitly below. Anything else - including user-defined types
// and pointers other than void* - falls back to mask_all, which reproduces
// the unmasked behaviour exactly.
//*************************************************************************
typedef unsigned int arg_mask_t;
static ETL_CONSTANT arg_mask_t mask_monostate = 1U << 0;
static ETL_CONSTANT arg_mask_t mask_bool = 1U << 1;
static ETL_CONSTANT arg_mask_t mask_char = 1U << 2;
static ETL_CONSTANT arg_mask_t mask_int = 1U << 3;
static ETL_CONSTANT arg_mask_t mask_uint = 1U << 4;
static ETL_CONSTANT arg_mask_t mask_llong = 1U << 5;
static ETL_CONSTANT arg_mask_t mask_ullong = 1U << 6;
static ETL_CONSTANT arg_mask_t mask_float = 1U << 7;
static ETL_CONSTANT arg_mask_t mask_double = 1U << 8;
static ETL_CONSTANT arg_mask_t mask_ldouble = 1U << 9;
static ETL_CONSTANT arg_mask_t mask_cstring = 1U << 10;
static ETL_CONSTANT arg_mask_t mask_string_view = 1U << 11;
static ETL_CONSTANT arg_mask_t mask_voidp = 1U << 12;
static ETL_CONSTANT arg_mask_t mask_all = ~static_cast<arg_mask_t>(0);
// Maps a type onto the set of alternatives a value of that type can be
// stored as. Mirrors the converting constructors of basic_format_arg.
//
// This serves both directions of the problem. For a user argument type it
// gives the alternatives that argument may end up in, which is how the
// per-call mask is built. For a variant alternative it degenerates to that
// alternative's own bit, which is how format_visitor tests reachability.
//
// The primary template is deliberately conservative: an unrecognised type
// yields mask_all, so it is never wrongly excluded.
template <typename T, typename Enable = void>
struct arg_type_mask
{
static ETL_CONSTANT arg_mask_t value = mask_all;
};
// Not constructible as a user argument; listed so that the table covers
// every variant alternative. Every mask contains this bit regardless, as
// the args_mask recursion bottoms out at mask_monostate.
template <>
struct arg_type_mask<etl::monostate, void>
{
static ETL_CONSTANT arg_mask_t value = mask_monostate;
};
template <>
struct arg_type_mask<bool, void>
{
static ETL_CONSTANT arg_mask_t value = mask_bool;
};
template <>
struct arg_type_mask<char, void>
{
static ETL_CONSTANT arg_mask_t value = mask_char;
};
template <>
struct arg_type_mask<signed char, void>
{
static ETL_CONSTANT arg_mask_t value = mask_int;
};
template <>
struct arg_type_mask<unsigned char, void>
{
static ETL_CONSTANT arg_mask_t value = mask_uint;
};
template <>
struct arg_type_mask<short, void>
{
static ETL_CONSTANT arg_mask_t value = mask_int;
};
template <>
struct arg_type_mask<unsigned short, void>
{
static ETL_CONSTANT arg_mask_t value = mask_uint;
};
template <>
struct arg_type_mask<int, void>
{
static ETL_CONSTANT arg_mask_t value = mask_int;
};
template <>
struct arg_type_mask<unsigned int, void>
{
static ETL_CONSTANT arg_mask_t value = mask_uint;
};
template <>
struct arg_type_mask<long int, void>
{
static ETL_CONSTANT arg_mask_t value = mask_llong;
};
template <>
struct arg_type_mask<unsigned long int, void>
{
static ETL_CONSTANT arg_mask_t value = mask_ullong;
};
template <>
struct arg_type_mask<long long int, void>
{
static ETL_CONSTANT arg_mask_t value = mask_llong;
};
template <>
struct arg_type_mask<unsigned long long int, void>
{
static ETL_CONSTANT arg_mask_t value = mask_ullong;
};
#if ETL_USING_FORMAT_FLOATING_POINT
template <>
struct arg_type_mask<float, void>
{
static ETL_CONSTANT arg_mask_t value = mask_float;
};
template <>
struct arg_type_mask<double, void>
{
static ETL_CONSTANT arg_mask_t value = mask_double;
};
template <>
struct arg_type_mask<long double, void>
{
static ETL_CONSTANT arg_mask_t value = mask_ldouble;
};
#endif
template <>
struct arg_type_mask<char*, void>
{
static ETL_CONSTANT arg_mask_t value = mask_cstring;
};
template <>
struct arg_type_mask<const char*, void>
{
static ETL_CONSTANT arg_mask_t value = mask_cstring;
};
template <>
struct arg_type_mask<etl::string_view, void>
{
static ETL_CONSTANT arg_mask_t value = mask_string_view;
};
template <>
struct arg_type_mask<void*, void>
{
static ETL_CONSTANT arg_mask_t value = mask_voidp;
};
template <>
struct arg_type_mask<const void*, void>
{
static ETL_CONSTANT arg_mask_t value = mask_voidp;
};
// etl::string<N>, etl::istring and friends are stored as a string_view.
template <typename T>
struct arg_type_mask<T, etl::enable_if_t<etl::is_base_of<etl::ibasic_string<char>, T>::value>>
{
static ETL_CONSTANT arg_mask_t value = mask_string_view;
};
// The empty alternative is always reachable, so monostate is always set.
template <typename... Ts>
struct args_mask;
template <>
struct args_mask<>
{
static ETL_CONSTANT arg_mask_t value = mask_monostate;
};
template <typename T, typename... Rest>
struct args_mask<T, Rest...>
{
static ETL_CONSTANT arg_mask_t value = arg_type_mask<etl::decay_t<T>>::value | args_mask<Rest...>::value;
};
} // namespace private_format
template <class Context>
@ -1206,11 +1397,11 @@ namespace etl
return false;
}
template <class OutputIt>
void parse_format_spec(format_parse_context& parse_ctx, format_context<OutputIt>& fmt_context)
// Not templated on the output iterator: the parse phase only reads the
// parse context and writes the format spec, so a single instantiation is
// shared by every output iterator type.
inline void parse_format_spec(format_parse_context& parse_ctx, format_spec_t& format_spec)
{
auto& format_spec = fmt_context.format_spec;
format_spec = format_spec_t(); // reset format_spec to defaults
format_spec.index = parse_num(parse_ctx); // optional explicit index
@ -1536,6 +1727,19 @@ namespace etl
}
#if ETL_USING_FORMAT_FLOATING_POINT
//***********************************
// On many targets (e.g. ARM EABI, MSVC) 'long double' has exactly the same
// representation as 'double'. There, formatting a long double through the
// 'double' instantiation is value-preserving, and avoids emitting a second,
// byte-identical copy of the whole floating point formatting chain.
// On targets where the two differ (e.g. x86 80-bit) 'long double' is kept.
typedef etl::conditional_t<(sizeof(long double) == sizeof(double))
&& (etl::numeric_limits<long double>::digits == etl::numeric_limits<double>::digits)
&& (etl::numeric_limits<long double>::max_exponent == etl::numeric_limits<double>::max_exponent)
&& (etl::numeric_limits<long double>::min_exponent == etl::numeric_limits<double>::min_exponent),
double, long double>
long_double_format_type;
#if ETL_NOT_USING_FORMAT_LONG_DOUBLE_MATH
//***********************************
// Math function wrappers to handle toolchains that don't provide
@ -2004,7 +2208,7 @@ namespace etl
}
#endif
template <class OutputIt>
template <class OutputIt, arg_mask_t Mask = mask_all>
struct format_visitor
{
using output_iterator = OutputIt;
@ -2019,11 +2223,12 @@ namespace etl
template <typename T>
void operator()(T value)
{
formatter<T> f;
format_parse_context::iterator it = f.parse(parse_ctx);
parse_ctx.advance_to(it);
OutputIt fit = f.format(value, fmt_ctx);
fmt_ctx.advance_to(fit);
// T is a variant alternative here, so arg_type_mask<T> is that
// alternative's own bit.
// Tag dispatch rather than 'if constexpr' so that the formatter for an
// unreachable alternative is not instantiated in C++11/14 either.
typedef etl::conditional_t<(arg_type_mask<T>::value & Mask) != 0, etl::true_type, etl::false_type> reachable;
dispatch(value, reachable());
}
// for user-defined types routed through basic_format_arg::handle
@ -2034,6 +2239,25 @@ namespace etl
format_parse_context& parse_ctx;
format_context<OutputIt>& fmt_ctx;
private:
template <typename T>
void dispatch(T value, etl::true_type)
{
formatter<T> f;
format_parse_context::iterator it = f.parse(parse_ctx);
parse_ctx.advance_to(it);
OutputIt fit = f.format(value, fmt_ctx);
fmt_ctx.advance_to(fit);
}
// This alternative cannot be reached for the argument types of this call,
// so no formatter is instantiated for it.
template <typename T>
void dispatch(T, etl::false_type)
{
}
};
template <class OutputIt>
@ -2645,7 +2869,8 @@ namespace etl
template <class OutputIt>
typename format_context<OutputIt>::iterator format(long double arg, format_context<OutputIt>& fmt_ctx)
{
return private_format::format_aligned_floating<OutputIt, long double>(arg, fmt_ctx);
typedef private_format::long_double_format_type type;
return private_format::format_aligned_floating<OutputIt, type>(static_cast<type>(arg), fmt_ctx);
}
};
#endif
@ -2715,12 +2940,15 @@ namespace etl
}
};
template <class OutputIt>
// Mask is a compile-time set of the basic_format_arg alternatives that the
// caller can actually supply. It only removes unreachable code paths; the
// default of mask_all formats every alternative, as before.
template <class OutputIt, private_format::arg_mask_t Mask = private_format::mask_all>
OutputIt vformat_to(OutputIt out, etl::string_view fmt, format_args<OutputIt> args)
{
format_parse_context parse_context(fmt, args.size());
format_context<OutputIt> fmt_context(out, args);
private_format::format_visitor<OutputIt> v(parse_context, fmt_context);
private_format::format_visitor<OutputIt, Mask> v(parse_context, fmt_context);
while (parse_context.begin() != parse_context.end())
{
@ -2736,7 +2964,7 @@ namespace etl
}
else
{
private_format::parse_format_spec<OutputIt>(parse_context, fmt_context);
private_format::parse_format_spec(parse_context, fmt_context.format_spec);
// Resolve nested replacement fields for width/precision
private_format::resolve_nested_replacements<OutputIt>(fmt_context.format_spec, args);
@ -2774,14 +3002,14 @@ namespace etl
OutputIt format_to(OutputIt out, format_string<Args...> fmt, Args&&... args)
{
auto the_args{make_format_args<OutputIt>(args...)};
return vformat_to(etl::move(out), fmt.get(), format_args<OutputIt>(the_args));
return vformat_to<OutputIt, private_format::args_mask<Args...>::value>(etl::move(out), fmt.get(), format_args<OutputIt>(the_args));
}
template <typename OutputIt, class WrapperIt = private_format::limit_iterator<OutputIt>, class... Args>
OutputIt format_to_n(OutputIt out, size_t n, format_string<Args...> fmt, Args&&... args)
{
auto the_args{make_format_args<WrapperIt>(args...)};
return vformat_to(WrapperIt(out, n), fmt.get(), format_args<WrapperIt>(the_args)).get();
return vformat_to<WrapperIt, private_format::args_mask<Args...>::value>(WrapperIt(out, n), fmt.get(), format_args<WrapperIt>(the_args)).get();
}
// non std in the following, specific to etl