[libc++] Use std::to_chars to format thread::id and canonicalize the representation across platforms (#181624)

This makes printing `thread::id` faster, since we avoid a bunch of
boilerplate code that isn't optimized away. It also avoids including
`<sstream>` and `<__locale>`, cutting the time to parse `<thread>` in
half.

This also changes the output when printing the id on a platform that
uses a pointer as the underlying type. I don't think that's a problem,
since the thread id isn't in any way stable. It also makes the output
consistent across all platforms we support.
This commit is contained in:
Nikolas Klauser 2026-07-05 19:36:39 +02:00 committed by GitHub
parent 4a02e116e5
commit a68147f0f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 170 additions and 194 deletions

View file

@ -92,6 +92,9 @@ Potentially breaking changes
including the correct header. To ease the transition ``_LIBCPP_KEEP_TRANSITIVE_INCLUDES_LLVM23`` can be defined, which
includes the removed headers again. This macro will be removed in LLVM 24.
- The output of ``ostream << std::thread::id`` and ``std::format("...", std::thread:id)`` changes on some platforms from
hexadecimal to decimal. This is done to have consistent formatting across all platforms libc++ supports.
Announcements About Future Releases
-----------------------------------

View file

@ -51,20 +51,13 @@ public:
// On Linux systems pthread_t is an unsigned long long.
// On Apple systems pthread_t is a pointer type.
//
// Note the output should match what the stream operator does. Since
// the ostream operator has been shipped years before this formatter
// was added to the Standard, this formatter does what the stream
// operator does. This may require platform specific changes.
// Note the output should match what the stream operator does.
using _Tp = decltype(__get_underlying_id(__id));
using _Cp = conditional_t<integral<_Tp>, _Tp, conditional_t<is_pointer_v<_Tp>, uintptr_t, void>>;
static_assert(!is_same_v<_Cp, void>, "unsupported thread::id type, please file a bug report");
__format_spec::__parsed_specifications<_CharT> __specs = __parser_.__get_parsed_std_specifications(__ctx);
if constexpr (is_pointer_v<_Tp>) {
__specs.__std_.__alternate_form_ = true;
__specs.__std_.__type_ = __format_spec::__type::__hexadecimal_lower_case;
}
return __formatter::__format_integer(reinterpret_cast<_Cp>(__get_underlying_id(__id)), __ctx, __specs);
}

View file

@ -11,12 +11,13 @@
#define _LIBCPP___THREAD_THREAD_H
#include <__assert>
#include <__charconv/to_chars_integral.h>
#include <__condition_variable/condition_variable.h>
#include <__config>
#include <__exception/terminate.h>
#include <__functional/hash.h>
#include <__functional/unary_function.h>
#include <__locale>
#include <__fwd/ostream.h>
#include <__memory/addressof.h>
#include <__memory/unique_ptr.h>
#include <__mutex/mutex.h>
@ -27,16 +28,15 @@
#include <__type_traits/enable_if.h>
#include <__type_traits/invoke.h>
#include <__type_traits/is_constructible.h>
#include <__type_traits/is_pointer.h>
#include <__type_traits/is_same.h>
#include <__type_traits/remove_cvref.h>
#include <__utility/exchange.h>
#include <__utility/forward.h>
#include <cstdint>
#include <limits>
#include <tuple>
#if _LIBCPP_HAS_LOCALIZATION
# include <sstream>
#endif
#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
# pragma GCC system_header
#endif
@ -146,13 +146,19 @@ operator<<(basic_ostream<_CharT, _Traits>& __os, __thread_id __id) {
//
// Since various flags in the output stream can affect how the
// thread id is represented (e.g. numpunct or showbase), we
// use a temporary stream instead and just output the thread
// id representation as a string.
// use `to_chars` directly and output the id as a string.
basic_ostringstream<_CharT, _Traits> __sstr;
__sstr.imbue(locale::classic());
__sstr << __id.__id_;
return __os << __sstr.str();
static_assert(is_pointer<__libcpp_thread_id>::value || is_integral<__libcpp_thread_id>::value);
using __int_type = __conditional_t<is_pointer<__libcpp_thread_id>::value, uintptr_t, __libcpp_thread_id>;
static const size_t __buffer_size = numeric_limits<__int_type>::digits10 + 2;
char __buffer[__buffer_size];
auto __ret =
std::__to_chars_integral(__buffer, __buffer + __buffer_size - 1, reinterpret_cast<__int_type>(__id.__id_), 10);
_LIBCPP_ASSERT_INTERNAL(__ret.__ec == std::errc(), "to_chars failed!");
*__ret.__ptr = '\0';
return __os << __buffer;
}
# endif // _LIBCPP_HAS_LOCALIZATION

View file

@ -2078,6 +2078,11 @@ _LIBCPP_POP_MACROS
# include <system_error>
# include <thread>
# endif
# if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 23
# include <sstream>
# endif
#endif // __cplusplus < 201103L && defined(_LIBCPP_USE_FROZEN_CXX03_HEADERS)
#endif // _LIBCPP_FUTURE

View file

@ -948,7 +948,10 @@ module std {
header "__charconv/to_chars_integral.h"
export std.charconv.to_chars_result
}
module to_chars_result { header "__charconv/to_chars_result.h" }
module to_chars_result {
header "__charconv/to_chars_result.h"
export std.system_error.errc
}
module traits { header "__charconv/traits.h" }
header "charconv"

View file

@ -131,6 +131,7 @@ void sleep_for(const chrono::duration<Rep, Period>& rel_time);
# if defined(_LIBCPP_KEEP_TRANSITIVE_INCLUDES_LLVM23) && _LIBCPP_STD_VER <= 23
# include <atomic>
# include <sstream>
# endif
#endif // __cplusplus < 201103L && defined(_LIBCPP_USE_FROZEN_CXX03_HEADERS)

View file

@ -0,0 +1,94 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
// UNSUPPORTED: no-threads
// UNSUPPORTED: GCC-ALWAYS_INLINE-FIXME
// TODO FMT This test should not require std::to_chars(floating-point)
// XFAIL: availability-fp_to_chars-missing
// <thread>
// template<class charT>
// struct formatter<thread::id, charT>;
// template<class... Args>
// string format(format_string<Args...> fmt, Args&&... args);
// template<class... Args>
// wstring format(wformat_string<Args...> fmt, Args&&... args);
#include <cassert>
#include <format>
#include <thread>
#include "assert_macros.h"
#include "concat_macros.h"
#include "format.functions.common.h"
#include "test_format_string.h"
#include "test_macros.h"
template <class CharT, class TestFunction>
void format_tests(TestFunction check) {
// Note the output of std::thread::id is unspecified. The output text is the
// same as the stream operator. Since that format is already released this
// test follows the practice on existing systems.
std::thread::id input{};
/***** Test the type specific part *****/
check(SV("0"), SV("{}"), input);
check(SV("0^42"), SV("{}^42"), input);
check(SV("0^42"), SV("{:}^42"), input);
// *** align-fill & width ***
check(SV(" 0"), SV("{:5}"), input);
check(SV("0****"), SV("{:*<5}"), input);
check(SV("__0__"), SV("{:_^5}"), input);
check(SV("::::0"), SV("{::>5}"), input); // This is not a range, so : is allowed as fill character.
check(SV(" 0"), SV("{:{}}"), input, 5);
check(SV("0****"), SV("{:*<{}}"), input, 5);
check(SV("__0__"), SV("{:_^{}}"), input, 5);
check(SV("####0"), SV("{:#>{}}"), input, 5);
}
auto test = []<class CharT, class... Args>(
std::basic_string_view<CharT> expected, test_format_string<CharT, Args...> fmt, Args&&... args) {
{
std::basic_string<CharT> out = std::format(fmt, std::forward<Args>(args)...);
TEST_REQUIRE(out == expected,
TEST_WRITE_CONCATENATED(
"\nFormat string ", fmt, "\nExpected output ", expected, "\nActual output ", out, '\n'));
}
{
std::basic_string<CharT> out = std::vformat(fmt.get(), std::make_format_args<context_t<CharT>>(args...));
TEST_REQUIRE(out == expected,
TEST_WRITE_CONCATENATED(
"\nFormat string ", fmt, "\nExpected output ", expected, "\nActual output ", out, '\n'));
}
};
int main(int, char**) {
format_tests<char>(test);
#ifndef TEST_HAS_NO_WIDE_CHARACTERS
format_tests<wchar_t>(test);
#endif
{ // Check that we print the ID correctly (i.e. library-internal round-tripping works)
auto thread_id = std::this_thread::get_id();
auto result = std::format("{}", thread_id);
auto num = std::stoull(result);
using int_t =
std::conditional_t<std::is_pointer<std::__libcpp_thread_id>::value, uintptr_t, std::__libcpp_thread_id>;
assert(reinterpret_cast<int_t>(__get_underlying_id(std::this_thread::get_id())) == static_cast<int_t>(num));
}
return 0;
}

View file

@ -423,32 +423,23 @@ functional tuple
functional typeinfo
functional unordered_map
functional version
future bitset
future cctype
future cerrno
future climits
future clocale
future compare
future cstddef
future cstdint
future cstdio
future cstdlib
future cstring
future ctime
future cwchar
future cwctype
future initializer_list
future ios
future iosfwd
future istream
future limits
future ratio
future sstream
future stdexcept
future streambuf
future string
future string_view
future text_encoding
future tuple
future typeinfo
future version
@ -1043,7 +1034,6 @@ text_encoding stdexcept
text_encoding string_view
text_encoding version
thread array
thread bitset
thread cctype
thread cerrno
thread climits
@ -1058,14 +1048,10 @@ thread ctime
thread cwchar
thread cwctype
thread initializer_list
thread ios
thread iosfwd
thread istream
thread limits
thread ratio
thread sstream
thread stdexcept
thread streambuf
thread string
thread string_view
thread text_encoding

1 algorithm cctype
423 functional typeinfo
424 functional unordered_map
425 functional version
future bitset
426 future cctype
427 future cerrno
428 future climits
future clocale
429 future compare
future cstddef
430 future cstdint
431 future cstdio
future cstdlib
432 future cstring
433 future ctime
434 future cwchar
435 future cwctype
436 future initializer_list
future ios
437 future iosfwd
future istream
438 future limits
439 future ratio
future sstream
440 future stdexcept
future streambuf
441 future string
442 future string_view
future text_encoding
443 future tuple
444 future typeinfo
445 future version
1034 text_encoding string_view
1035 text_encoding version
1036 thread array
thread bitset
1037 thread cctype
1038 thread cerrno
1039 thread climits
1048 thread cwchar
1049 thread cwctype
1050 thread initializer_list
thread ios
1051 thread iosfwd
thread istream
1052 thread limits
1053 thread ratio
thread sstream
1054 thread stdexcept
thread streambuf
1055 thread string
1056 thread string_view
1057 thread text_encoding

View file

@ -1,57 +0,0 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
// UNSUPPORTED: no-threads
// UNSUPPORTED: GCC-ALWAYS_INLINE-FIXME
// TODO FMT This test should not require std::to_chars(floating-point)
// XFAIL: availability-fp_to_chars-missing
// <thread>
// template<class charT>
// struct formatter<thread::id, charT>;
// template<class... Args>
// string format(format_string<Args...> fmt, Args&&... args);
// template<class... Args>
// wstring format(wformat_string<Args...> fmt, Args&&... args);
#include <format>
#include <cassert>
#include "assert_macros.h"
#include "concat_macros.h"
#include "format.functions.tests.h"
#include "test_format_string.h"
#include "test_macros.h"
auto test = []<class CharT, class... Args>(
std::basic_string_view<CharT> expected, test_format_string<CharT, Args...> fmt, Args&&... args) {
std::basic_string<CharT> out = std::format(fmt, std::forward<Args>(args)...);
TEST_REQUIRE(out == expected,
TEST_WRITE_CONCATENATED(
"\nFormat string ", fmt, "\nExpected output ", expected, "\nActual output ", out, '\n'));
};
auto test_exception = []<class CharT, class... Args>(std::string_view, std::basic_string_view<CharT>, Args&&...) {
// After P2216 most exceptions thrown by std::format become ill-formed.
// Therefore this tests does nothing.
};
int main(int, char**) {
format_tests<char>(test, test_exception);
#ifndef TEST_HAS_NO_WIDE_CHARACTERS
format_tests<wchar_t>(test, test_exception);
#endif
return 0;
}

View file

@ -1,83 +0,0 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef TEST_STD_THREAD_THREAD_THREADS_THREAD_THREAD_CLASS_THREAD_THREAD_ID_FORMAT_FUNCTIONS_TESTS_H
#define TEST_STD_THREAD_THREAD_THREADS_THREAD_THREAD_CLASS_THREAD_THREAD_ID_FORMAT_FUNCTIONS_TESTS_H
#include <thread>
#include "format.functions.common.h"
#include "test_macros.h"
template <class CharT, class TestFunction, class ExceptionTest>
void format_tests(TestFunction check, ExceptionTest check_exception) {
// Note the output of std::thread::id is unspecified. The output text is the
// same as the stream operator. Since that format is already released this
// test follows the practice on existing systems.
std::thread::id input{};
/***** Test the type specific part *****/
#if !defined(__APPLE__) && !defined(__FreeBSD__)
check(SV("0"), SV("{}"), input);
check(SV("0^42"), SV("{}^42"), input);
check(SV("0^42"), SV("{:}^42"), input);
// *** align-fill & width ***
check(SV(" 0"), SV("{:5}"), input);
check(SV("0****"), SV("{:*<5}"), input);
check(SV("__0__"), SV("{:_^5}"), input);
check(SV("::::0"), SV("{::>5}"), input); // This is not a range, so : is allowed as fill character.
check(SV(" 0"), SV("{:{}}"), input, 5);
check(SV("0****"), SV("{:*<{}}"), input, 5);
check(SV("__0__"), SV("{:_^{}}"), input, 5);
check(SV("####0"), SV("{:#>{}}"), input, 5);
#else // !defined(__APPLE__) && !defined(__FreeBSD__)
check(SV("0x0"), SV("{}"), input);
check(SV("0x0^42"), SV("{}^42"), input);
check(SV("0x0^42"), SV("{:}^42"), input);
// *** align-fill & width ***
check(SV(" 0x0"), SV("{:7}"), input);
check(SV("0x0****"), SV("{:*<7}"), input);
check(SV("__0x0__"), SV("{:_^7}"), input);
check(SV("::::0x0"), SV("{::>7}"), input); // This is not a range, so : is allowed as fill character.
check(SV(" 0x0"), SV("{:{}}"), input, 7);
check(SV("0x0****"), SV("{:*<{}}"), input, 7);
check(SV("__0x0__"), SV("{:_^{}}"), input, 7);
check(SV("####0x0"), SV("{:#>{}}"), input, 7);
#endif // !defined(__APPLE__) && !defined(__FreeBSD__)
/***** Test the type generic part *****/
check_exception("The format string contains an invalid escape sequence", SV("{:}<}"), input);
check_exception("The fill option contains an invalid value", SV("{:{<}"), input);
// *** sign ***
check_exception("The replacement field misses a terminating '}'", SV("{:-}"), input);
check_exception("The replacement field misses a terminating '}'", SV("{:+}"), input);
check_exception("The replacement field misses a terminating '}'", SV("{: }"), input);
// *** alternate form ***
check_exception("The replacement field misses a terminating '}'", SV("{:#}"), input);
// *** zero-padding ***
check_exception("The width option should not have a leading zero", SV("{:0}"), input);
// *** precision ***
check_exception("The replacement field misses a terminating '}'", SV("{:.}"), input);
// *** locale-specific form ***
check_exception("The replacement field misses a terminating '}'", SV("{:L}"), input);
// *** type ***
for (std::basic_string_view<CharT> fmt : fmt_invalid_types<CharT>(""))
check_exception("The replacement field misses a terminating '}'", fmt, input);
}
#endif // TEST_STD_THREAD_THREAD_THREADS_THREAD_THREAD_CLASS_THREAD_THREAD_ID_FORMAT_FUNCTIONS_TESTS_H

View file

@ -22,21 +22,47 @@
// string vformat(string_view fmt, format_args args);
// wstring vformat(wstring_view fmt, wformat_args args);
#include <format>
#include <cassert>
#include <format>
#include <thread>
#include "assert_macros.h"
#include "concat_macros.h"
#include "format.functions.tests.h"
#include "format.functions.common.h"
#include "test_macros.h"
auto test = []<class CharT, class... Args>(
std::basic_string_view<CharT> expected, std::basic_string_view<CharT> fmt, Args&&... args) {
std::basic_string<CharT> out = std::vformat(fmt, std::make_format_args<context_t<CharT>>(args...));
TEST_REQUIRE(out == expected,
TEST_WRITE_CONCATENATED(
"\nFormat string ", fmt, "\nExpected output ", expected, "\nActual output ", out, '\n'));
};
template <class CharT, class ExceptionTest>
void format_tests(ExceptionTest check_exception) {
// Note the output of std::thread::id is unspecified. The output text is the
// same as the stream operator. Since that format is already released this
// test follows the practice on existing systems.
std::thread::id input{};
/***** Test the type generic part *****/
check_exception("The format string contains an invalid escape sequence", SV("{:}<}"), input);
check_exception("The fill option contains an invalid value", SV("{:{<}"), input);
// *** sign ***
check_exception("The replacement field misses a terminating '}'", SV("{:-}"), input);
check_exception("The replacement field misses a terminating '}'", SV("{:+}"), input);
check_exception("The replacement field misses a terminating '}'", SV("{: }"), input);
// *** alternate form ***
check_exception("The replacement field misses a terminating '}'", SV("{:#}"), input);
// *** zero-padding ***
check_exception("The width option should not have a leading zero", SV("{:0}"), input);
// *** precision ***
check_exception("The replacement field misses a terminating '}'", SV("{:.}"), input);
// *** locale-specific form ***
check_exception("The replacement field misses a terminating '}'", SV("{:L}"), input);
// *** type ***
for (std::basic_string_view<CharT> fmt : fmt_invalid_types<CharT>(""))
check_exception("The replacement field misses a terminating '}'", fmt, input);
}
auto test_exception =
[]<class CharT, class... Args>(
@ -55,10 +81,10 @@ auto test_exception =
};
int main(int, char**) {
format_tests<char>(test, test_exception);
format_tests<char>(test_exception);
#ifndef TEST_HAS_NO_WIDE_CHARACTERS
format_tests<wchar_t>(test, test_exception);
format_tests<wchar_t>(test_exception);
#endif
return 0;

View file

@ -34,9 +34,8 @@
#define SV(S) MAKE_STRING_VIEW(CharT, S)
template <class StringViewT>
void test_format(StringViewT expected, std::thread::id arg) {
using CharT = typename StringViewT::value_type;
template <class CharT>
std::basic_string<CharT> test_format(std::thread::id arg) {
using String = std::basic_string<CharT>;
using OutIt = std::back_insert_iterator<String>;
using FormatCtxT = std::basic_format_context<OutIt, CharT>;
@ -47,16 +46,15 @@ void test_format(StringViewT expected, std::thread::id arg) {
OutIt out = std::back_inserter(result);
FormatCtxT format_ctx = test_format_context_create<OutIt, CharT>(out, std::make_format_args<FormatCtxT>(arg));
formatter.format(arg, format_ctx);
assert(result == expected);
assert(!result.empty());
return result;
}
template <class CharT>
void test_fmt() {
#if !defined(__APPLE__) && !defined(__FreeBSD__)
test_format(SV("0"), std::thread::id());
#else
test_format(SV("0x0"), std::thread::id());
#endif
auto def = test_format<CharT>(std::thread::id());
auto self = test_format<CharT>(std::this_thread::get_id());
assert(def != self);
}
void test() {

View file

@ -27,6 +27,7 @@
#include <format>
#include <locale>
#include <sstream>
#include <string>
#include <thread>
#include "make_string.h"