Fix formatting of int8_t and uint8_t (#1551)

Fixes https://github.com/ETLCPP/etl/issues/1549
This commit is contained in:
Roland Reichwein 2026-08-14 08:59:17 +02:00 committed by GitHub
parent 7ee5a661b6
commit cc91f2ed78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 77 additions and 59 deletions

View File

@ -91,44 +91,70 @@ namespace etl
POINTER // const void*
};
// Map a type to its category. Decays and removes cv-qualifiers.
// Trait mapping a type (with references and cv-qualifiers removed) to its type_category.
//
// NOTE: signed char/unsigned char (typically int8_t/uint8_t) are NOT
// categorised as CHAR here - only plain 'char' gets character semantics.
// signed/unsigned char are formatted as integers, matching basic_format_arg
// storage and std::format's formatter<signed char>/<unsigned char>. This is
// enforced structurally: the INTEGER partial specialisation is constrained
// with is_integral<T> && !is_same<T, bool> && !is_same<T, char>, so
// signed/unsigned char (is_integral == true) fall into INTEGER while the
// dedicated bool/char full specialisations take priority for those exact types.
template <class T, class = void>
struct type_category_trait
{
static constexpr type_category value = type_category::NONE; // unknown type: custom formatter, be permissive
};
template <>
struct type_category_trait<bool>
{
static constexpr type_category value = type_category::BOOLEAN;
};
template <>
struct type_category_trait<char>
{
static constexpr type_category value = type_category::CHAR;
};
template <class T>
struct type_category_trait<
T, typename etl::enable_if<etl::is_integral<T>::value && !etl::is_same<T, bool>::value && !etl::is_same<T, char>::value>::type>
{
static constexpr type_category value = type_category::INTEGER;
};
template <class T>
struct type_category_trait<T, typename etl::enable_if<etl::is_floating_point<T>::value>::type>
{
static constexpr type_category value = type_category::FLOAT;
};
template <class T>
struct type_category_trait<T,
typename etl::enable_if<etl::is_same<T, char*>::value || etl::is_same<T, const char*>::value
|| etl::is_same<T, etl::string_view>::value || etl::is_base_of<etl::istring, T>::value>::type>
{
static constexpr type_category value = type_category::STRING;
};
template <class T>
struct type_category_trait<
T, typename etl::enable_if<etl::is_pointer<T>::value && !etl::is_same<T, char*>::value && !etl::is_same<T, const char*>::value>::type>
{
static constexpr type_category value = type_category::POINTER;
};
// Map a type to its category. Removes references and cv-qualifiers, then defers
// entirely to the type_category_trait specialisations above.
template <class T>
constexpr type_category get_type_category()
{
using U = typename etl::remove_cv<typename etl::remove_reference<T>::type>::type;
// Order matters: bool before integral, char before integral
if (etl::is_same<U, bool>::value)
return type_category::BOOLEAN;
if (etl::is_same<U, char>::value)
return type_category::CHAR;
if (etl::is_same<U, signed char>::value)
return type_category::CHAR;
if (etl::is_same<U, unsigned char>::value)
return type_category::CHAR;
if (etl::is_integral<U>::value)
return type_category::INTEGER;
if (etl::is_same<U, float>::value)
return type_category::FLOAT;
if (etl::is_same<U, double>::value)
return type_category::FLOAT;
if (etl::is_same<U, long double>::value)
return type_category::FLOAT;
if (etl::is_same<U, const char*>::value)
return type_category::STRING;
if (etl::is_same<U, char*>::value)
return type_category::STRING;
if (etl::is_same<U, etl::string_view>::value)
return type_category::STRING;
if (etl::is_base_of<etl::istring, U>::value)
return type_category::STRING;
if (etl::is_pointer<U>::value)
return type_category::POINTER;
if (etl::is_same<U, const void*>::value)
return type_category::POINTER;
if (etl::is_same<U, void*>::value)
return type_category::POINTER;
return type_category::NONE; // unknown type: custom formatter, be permissive
return type_category_trait<U>::value;
}
// Check if a format type character is valid for a given type category.
@ -727,13 +753,16 @@ namespace etl
{
}
// int8_t / uint8_t are typically defined as signed char / unsigned char, but
// (unlike plain char) they are formatted as integers by default, matching
// std::format's formatter<signed char>/formatter<unsigned char> behaviour.
basic_format_arg(const signed char v)
: data(static_cast<char>(v))
: data(static_cast<int>(v))
{
}
basic_format_arg(const unsigned char v)
: data(static_cast<char>(v))
: data(static_cast<unsigned int>(v))
{
}

View File

@ -218,10 +218,9 @@ namespace
{
etl::string<100> s;
// mapped to unsigned char
// CHECK_EQUAL("34", test_format(s, "{}", static_cast<uint8_t>(34)));
// mapped to signed char
// CHECK_EQUAL("-14", test_format(s, "{}", static_cast<int8_t>(-14)));
// int8_t/uint8_t are formatted as integers (issue #1549), not as characters
CHECK_EQUAL("34", test_format(s, "{}", static_cast<uint8_t>(34)));
CHECK_EQUAL("-14", test_format(s, "{}", static_cast<int8_t>(-14)));
CHECK_EQUAL("6534", test_format(s, "{}", static_cast<uint16_t>(6534)));
CHECK_EQUAL("-9414", test_format(s, "{}", static_cast<int16_t>(-9414)));
CHECK_EQUAL("236534", test_format(s, "{}", static_cast<uint32_t>(236534)));
@ -619,45 +618,35 @@ namespace
//*************************************************************************
TEST(test_format_signed_char)
{
// signed char (typically int8_t) is formatted as an integer by default
// (issue #1549), matching std::format. Only {:c} converts it to a character.
// '?' (debug-escape) is NOT a valid presentation type for signed char.
etl::string<100> s;
CHECK_EQUAL("a s b", test_format(s, "a {} b", static_cast<signed char>('s')));
CHECK_EQUAL("a 115 b", test_format(s, "a {} b", static_cast<signed char>('s')));
CHECK_EQUAL("a s b", test_format(s, "a {:c} b", static_cast<signed char>('s')));
CHECK_EQUAL("a 's' b", test_format(s, "a {:?} b", static_cast<signed char>('s')));
CHECK_EQUAL("a \t b", test_format(s, "a {} b", static_cast<signed char>('\t')));
CHECK_EQUAL("a '\\t' b", test_format(s, "a {:?} b", static_cast<signed char>('\t')));
CHECK_EQUAL("a '\\n' b", test_format(s, "a {:?} b", static_cast<signed char>('\n')));
CHECK_EQUAL("a '\\r' b", test_format(s, "a {:?} b", static_cast<signed char>('\r')));
CHECK_EQUAL("a '\\\"' b", test_format(s, "a {:?} b", static_cast<signed char>('"')));
CHECK_EQUAL("a '\\'' b", test_format(s, "a {:?} b", static_cast<signed char>('\'')));
CHECK_EQUAL("a '\\\\' b", test_format(s, "a {:?} b", static_cast<signed char>('\\')));
CHECK_EQUAL("a '\\\\' b", test_format(s, "a {:?} b", static_cast<signed char>('\\')));
CHECK_EQUAL("a 97 b", test_format(s, "a {:d} b", static_cast<signed char>('a')));
CHECK_EQUAL("a 61 b", test_format(s, "a {:X} b", static_cast<signed char>('a')));
CHECK_EQUAL("a 61 b", test_format(s, "a {:x} b", static_cast<signed char>('a')));
CHECK_EQUAL("a 0x61 b", test_format(s, "a {:#x} b", static_cast<signed char>('a')));
CHECK_EQUAL("a -12 b", test_format(s, "a {} b", static_cast<signed char>(-12)));
}
//*************************************************************************
TEST(test_format_unsigned_char)
{
// unsigned char (typically uint8_t) is formatted as an integer by default
// (issue #1549), matching std::format. Only {:c} converts it to a character.
// '?' (debug-escape) is NOT a valid presentation type for unsigned char.
etl::string<100> s;
CHECK_EQUAL("a s b", test_format(s, "a {} b", static_cast<unsigned char>('s')));
CHECK_EQUAL("a 115 b", test_format(s, "a {} b", static_cast<unsigned char>('s')));
CHECK_EQUAL("a s b", test_format(s, "a {:c} b", static_cast<unsigned char>('s')));
CHECK_EQUAL("a 's' b", test_format(s, "a {:?} b", static_cast<unsigned char>('s')));
CHECK_EQUAL("a \t b", test_format(s, "a {} b", static_cast<unsigned char>('\t')));
CHECK_EQUAL("a '\\t' b", test_format(s, "a {:?} b", static_cast<unsigned char>('\t')));
CHECK_EQUAL("a '\\n' b", test_format(s, "a {:?} b", static_cast<unsigned char>('\n')));
CHECK_EQUAL("a '\\r' b", test_format(s, "a {:?} b", static_cast<unsigned char>('\r')));
CHECK_EQUAL("a '\\\"' b", test_format(s, "a {:?} b", static_cast<unsigned char>('"')));
CHECK_EQUAL("a '\\'' b", test_format(s, "a {:?} b", static_cast<unsigned char>('\'')));
CHECK_EQUAL("a '\\\\' b", test_format(s, "a {:?} b", static_cast<unsigned char>('\\')));
CHECK_EQUAL("a '\\\\' b", test_format(s, "a {:?} b", static_cast<unsigned char>('\\')));
CHECK_EQUAL("a 97 b", test_format(s, "a {:d} b", static_cast<unsigned char>('a')));
CHECK_EQUAL("a 61 b", test_format(s, "a {:X} b", static_cast<unsigned char>('a')));
CHECK_EQUAL("a 61 b", test_format(s, "a {:x} b", static_cast<unsigned char>('a')));
CHECK_EQUAL("a 0x61 b", test_format(s, "a {:#x} b", static_cast<unsigned char>('a')));
CHECK_EQUAL("a 65 b", test_format(s, "a {} b", static_cast<unsigned char>(65)));
}
//*************************************************************************