Fix CI failures in from_chars locale handling (PR #5237)

Three issues in the extended-locale number parsing were breaking CI:

- On Windows, _create_locale() was called with LC_ALL_MASK, a POSIX-only
  constant that doesn't exist in the Windows CRT; it needs LC_ALL there.
- `::isspace_l`/`::_isspace_l` were called with a leading `::`, but on
  glibc, mingw-w64 and some other C libraries these are macros that expand
  to a parenthesized expression, so `::` followed by the expansion is a
  syntax error ("expected unqualified-id"). Dropping the `::` fixes this
  on all affected toolchains (gcc/clang on Linux, nvcc, mingw).
- The Windows implementation assumed the full `_<funcname>_l` family is
  always available, but some MinGW toolchains only provide a subset
  (e.g. missing `_strtoll_l`/`_strtoull_l`/`_strtof_l`/`_strtold_l`).
  The extended-locale fast path is now only used for genuine MSVC
  (including clang-cl, which mimics the MSVC ABI/CRT); other Windows
  toolchains fall back to the locale-dependent standard functions.

Also fixes a clang-tidy hicpp-named-parameter/readability-named-parameter
warning-as-error on two unnamed `init_val_t` test parameters.

Verified locally with gcc/clang on Linux, clang++ in C++20 mode, and
mingw-w64 (both g++ and clang targeting x86_64-w64-mingw32).
This commit is contained in:
Claude
2026-07-27 12:49:55 +00:00
parent 5b5ba1ac35
commit 4ce130af4e
3 changed files with 136 additions and 46 deletions
@@ -190,6 +190,58 @@ inline from_chars_result from_chars(const char* first, const char* last, T& valu
#if defined(_WIN32)
/*!
@brief Extended locale functions on Windows
A trait object which provides wrappers for isspace and the strto* family of
functions, based on the platform's support for extended locale APIs.
This is the Windows implementation. The primary class template definition
falls back to the standard locale-dependent functions, which is used on
MinGW, whose C runtime only provides an incomplete subset of the
`_<funcname>_l` family (e.g., missing `_strtof_l`/`_strtold_l` depending on
the toolchain version). A specialization for genuine MSVC (including
clang-cl, which mimics the MSVC ABI and CRT) is used instead, as the full
`_<funcname>_l` family has reliably been available there since at least
Visual Studio 2005.
*/
template <typename = const char*, typename = float>
struct extended_locale_traits
{
static constexpr bool is_locale_independent = false;
JSON_HEDLEY_PURE
static char get_decimal_point() noexcept
{
const auto* loc = localeconv();
JSON_ASSERT(loc != nullptr);
return (loc->decimal_point == nullptr) ? '.' : *(loc->decimal_point);
}
static int isspace(int c) noexcept
{
return std::isspace(c);
}
// NOLINTNEXTLINE(runtime/int)
static long long strtoll(const char* str, char** endptr) noexcept
{
return std::strtoll(str, endptr, 10);
}
// NOLINTNEXTLINE(runtime/int)
static unsigned long long strtoull(const char* str, char** endptr) noexcept
{
return std::strtoull(str, endptr, 10);
}
static constexpr float(&strtof)(const char*, char**) = std::strtof;
static constexpr double(&strtod)(const char*, char**) = std::strtod;
static constexpr long double(&strtold)(const char*, char**) = std::strtold;
};
#if defined(_MSC_VER)
/*!
@brief Get a pointer to a globally shared instance of the "C" locale on Windows
@@ -208,58 +260,51 @@ inline _locale_t get_c_locale_t() noexcept
}
};
using LocaleUPtr = std::unique_ptr<std::remove_pointer<_locale_t>::type, LocaleTDeleter>;
static const LocaleUPtr c_locale = LocaleUPtr{::_create_locale(LC_ALL_MASK, "C"), LocaleTDeleter{}};
static const LocaleUPtr c_locale = LocaleUPtr{::_create_locale(LC_ALL, "C"), LocaleTDeleter{}};
return c_locale.get();
}
/*!
@brief Extended locale functions on Windows
A trait object which provides wrappers for isspace and the strto* family of
functions, based on the platform's support for extended locale APIs.
This is the Windows implementation where extended locale support is always
available through the `_<funcname>_l` variants.
*/
template <typename = const char*, typename = float>
struct extended_locale_traits
template <typename T>
struct extended_locale_traits<T, float>
{
static constexpr bool is_locale_independent = true;
static constexpr char(&get_decimal_point)() = get_locale_independent_decimal_point;
static int isspace(int c) noexcept
{
return ::_isspace_l(c, get_c_locale_t());
return _isspace_l(c, get_c_locale_t());
}
// NOLINTNEXTLINE(runtime/int)
static long long strtoll(const char* str, char** endptr) noexcept
{
return ::_strtoll_l(str, endptr, 10, get_c_locale_t());
return _strtoll_l(str, endptr, 10, get_c_locale_t());
}
// NOLINTNEXTLINE(runtime/int)
static unsigned long long strtoull(const char* str, char** endptr) noexcept
{
return ::_strtoull_l(str, endptr, 10, get_c_locale_t());
return _strtoull_l(str, endptr, 10, get_c_locale_t());
}
static float strtof(const char* str, char** str_end) noexcept
static float strtof(T str, char** str_end)
{
return ::_strtof_l(str, str_end, get_c_locale_t());
return _strtof_l(str, str_end, get_c_locale_t());
}
static double strtod(const char* str, char** str_end) noexcept
static double strtod(T str, char** str_end)
{
return ::_strtod_l(str, str_end, get_c_locale_t());
return _strtod_l(str, str_end, get_c_locale_t());
}
static long double strtold(const char* str, char** str_end) noexcept
static long double strtold(T str, char** str_end)
{
return ::_strtold_l(str, str_end, get_c_locale_t());
return _strtold_l(str, str_end, get_c_locale_t());
}
};
#endif
#else
/*!
@@ -360,7 +405,7 @@ struct extended_locale_traits<T, decltype(strtof_l(
static int isspace(int c) noexcept
{
return ::isspace_l(c, get_c_locale_t());
return isspace_l(c, get_c_locale_t());
}
// NOLINTNEXTLINE(runtime/int)