Skip to content

Conversation

smallp-o-p
Copy link
Contributor

@smallp-o-p smallp-o-p commented Jul 15, 2025

- Replaces the use of std::optional in __format/format_context.h with a simple re-implementation that caters to std::locale only.

Why?

While working on #105430 I ran into an issue implementing [optional.syn] because of a circular include that looked like the following: optional -> __format/range_default_formatter.h -> __format/range_formatter.h -> __format/format_context.h -> optional. Only format_kind and range_format are needed, and so they looked like candidates to be put into an internal header.

### Other approaches considered

The first solution was to simply drop std::optional altogether, but it resulted in a pretty significant performance drop (numbers to come...) when compared to the 'lazy-load' implementation when running the format.bench.pass.cpp benchmark.

### Performance differences
From the benchmark runs I've done, the difference is minimal if not very slightly worse(?)
I've attached an example benchmark run of format.bench.pass.cpp, fmt_baseline is how it's currently done and fmt_candidate are my changes. I will have to do a few more runs to get a better picture.

fmt_bench_diff.txt
fmt_candidate.txt
fmt_baseline.txt

  • Granularize std::range_format and std::format_kind declarations into a header.

@smallp-o-p smallp-o-p requested a review from a team as a code owner July 15, 2025 15:56
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Jul 15, 2025
@llvmbot
Copy link
Member

llvmbot commented Jul 15, 2025

@llvm/pr-subscribers-libcxx

Author: William Tran-Viet (smallp-o-p)

Changes
  • Replaces the use of std::optional in __format/format_context.h with a simple re-implementation that caters to std::locale only.

Why?

While working on #105430 I ran into an issue implementing [optional.syn] because of a circular #include &lt;optional&gt; in __format/format_context.h, since I need __format/range_default_formatter.h inside optional. I originally implemented this in the `<optional> changes, but decided to split it up to make it easier to review.

Other approaches considered

The first solution was to simply drop std::optional altogether, but it resulted in a pretty significant performance drop (numbers to come...) when compared to the 'lazy-load' implementation when running the format.bench.pass.cpp benchmark.

Performance differences

From the benchmark runs I've done, the difference is minimal if not very slightly worse(?)
I've attached an example benchmark run of format.bench.pass.cpp, fmt_baseline is how it's currently done and fmt_candidate are my changes. I will have to do a few more runs to get a better picture.

fmt_bench_diff.txt
fmt_candidate.txt
fmt_baseline.txt


Full diff: https://github.com/llvm/llvm-project/pull/148876.diff

1 Files Affected:

  • (modified) libcxx/include/__format/format_context.h (+60-13)
diff --git a/libcxx/include/__format/format_context.h b/libcxx/include/__format/format_context.h
index e672ee7ad0581..59bd18f38bf12 100644
--- a/libcxx/include/__format/format_context.h
+++ b/libcxx/include/__format/format_context.h
@@ -21,12 +21,12 @@
 #include <__iterator/back_insert_iterator.h>
 #include <__iterator/concepts.h>
 #include <__memory/addressof.h>
+#include <__memory/construct_at.h>
 #include <__utility/move.h>
 #include <__variant/monostate.h>
 
 #if _LIBCPP_HAS_LOCALIZATION
 #  include <__locale>
-#  include <optional>
 #endif
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
@@ -45,6 +45,57 @@ template <class _OutIt, class _CharT>
 class basic_format_context;
 
 #  if _LIBCPP_HAS_LOCALIZATION
+
+/*
+ * Off-brand std::optional<locale> to avoid having to #include <optional>. This is necessary because <optional> needs
+ * range_format for P3168R2, which would cause an include cycle.
+ */
+
+class __optional_locale {
+private:
+  union {
+    std::locale __loc_;
+    char __null_state_ = '\0';
+  };
+  bool __has_value_ = false;
+
+public:
+  _LIBCPP_HIDE_FROM_ABI __optional_locale() noexcept {}
+  _LIBCPP_HIDE_FROM_ABI __optional_locale(const std::locale& __loc) noexcept : __loc_(__loc), __has_value_(true) {}
+  _LIBCPP_HIDE_FROM_ABI __optional_locale(std::locale&& __loc) noexcept
+      : __loc_(std::move(__loc)), __has_value_(true) {}
+  _LIBCPP_HIDE_FROM_ABI __optional_locale(__optional_locale&& __other_loc) noexcept
+      : __has_value_(__other_loc.__has_value_) {
+    if (__other_loc.__has_value_) {
+      std::__construct_at(&__loc_, std::move(__other_loc.__loc_));
+    }
+  }
+
+  _LIBCPP_HIDE_FROM_ABI ~__optional_locale() {
+    if (__has_value_) {
+      __loc_.~locale();
+    }
+  }
+
+  _LIBCPP_HIDE_FROM_ABI __optional_locale& operator=(std::locale&& __loc) noexcept {
+    if (__has_value_) {
+      __loc_ = std::move(__loc);
+    } else {
+      std::__construct_at(&__loc_, std::move(__loc));
+      __has_value_ = true;
+    }
+    return *this;
+  }
+
+  _LIBCPP_HIDE_FROM_ABI std::locale& __value() noexcept {
+    if (!__has_value_) {
+      __has_value_ = true;
+      std::__construct_at(&__loc_);
+    }
+    return __loc_;
+  }
+};
+
 /**
  * Helper to create a basic_format_context.
  *
@@ -54,7 +105,7 @@ template <class _OutIt, class _CharT>
 _LIBCPP_HIDE_FROM_ABI basic_format_context<_OutIt, _CharT>
 __format_context_create(_OutIt __out_it,
                         basic_format_args<basic_format_context<_OutIt, _CharT>> __args,
-                        optional<std::locale>&& __loc = nullopt) {
+                        __optional_locale&& __loc = __optional_locale()) {
   return std::basic_format_context(std::move(__out_it), __args, std::move(__loc));
 }
 #  else
@@ -72,8 +123,8 @@ using wformat_context = basic_format_context< back_insert_iterator<__format::__o
 
 template <class _OutIt, class _CharT>
   requires output_iterator<_OutIt, const _CharT&>
-class _LIBCPP_PREFERRED_NAME(format_context)
-    _LIBCPP_IF_WIDE_CHARACTERS(_LIBCPP_PREFERRED_NAME(wformat_context)) basic_format_context {
+class _LIBCPP_PREFERRED_NAME(format_context) _LIBCPP_IF_WIDE_CHARACTERS(_LIBCPP_PREFERRED_NAME(wformat_context))
+    basic_format_context {
 public:
   using iterator  = _OutIt;
   using char_type = _CharT;
@@ -84,11 +135,7 @@ class _LIBCPP_PREFERRED_NAME(format_context)
     return __args_.get(__id);
   }
 #  if _LIBCPP_HAS_LOCALIZATION
-  _LIBCPP_HIDE_FROM_ABI std::locale locale() {
-    if (!__loc_)
-      __loc_ = std::locale{};
-    return *__loc_;
-  }
+  _LIBCPP_HIDE_FROM_ABI std::locale locale() { return __loc_.__value(); }
 #  endif
   _LIBCPP_HIDE_FROM_ABI iterator out() { return std::move(__out_it_); }
   _LIBCPP_HIDE_FROM_ABI void advance_to(iterator __it) { __out_it_ = std::move(__it); }
@@ -106,16 +153,16 @@ class _LIBCPP_PREFERRED_NAME(format_context)
   // This is done by storing the locale of the constructor in this optional. If
   // locale() is called and the optional has no value the value will be created.
   // This allows the implementation to lazily create the locale.
-  // TODO FMT Validate whether lazy creation is the best solution.
-  optional<std::locale> __loc_;
+
+  __optional_locale __loc_;
 
   template <class _OtherOutIt, class _OtherCharT>
   friend _LIBCPP_HIDE_FROM_ABI basic_format_context<_OtherOutIt, _OtherCharT> __format_context_create(
-      _OtherOutIt, basic_format_args<basic_format_context<_OtherOutIt, _OtherCharT>>, optional<std::locale>&&);
+      _OtherOutIt, basic_format_args<basic_format_context<_OtherOutIt, _OtherCharT>>, __optional_locale&&);
 
   // Note: the Standard doesn't specify the required constructors.
   _LIBCPP_HIDE_FROM_ABI explicit basic_format_context(
-      _OutIt __out_it, basic_format_args<basic_format_context> __args, optional<std::locale>&& __loc)
+      _OutIt __out_it, basic_format_args<basic_format_context> __args, __optional_locale&& __loc)
       : __out_it_(std::move(__out_it)), __args_(__args), __loc_(std::move(__loc)) {}
 #  else
   template <class _OtherOutIt, class _OtherCharT>

Copy link
Contributor

@philnik777 philnik777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a good idea. Could you elaborate on where there is a circular dependency? AFAICT You only need to specialize two variables, which definitely doesn't have to cause circular dependencies.

@ldionne
Copy link
Member

ldionne commented Jul 15, 2025

I tend to agree: I would try to break the circular dependency some other way (e.g. a forward declaration header) in order to avoid reimplementing optional.

@smallp-o-p
Copy link
Contributor Author

I don't think this is a good idea. Could you elaborate on where there is a circular dependency? AFAICT You only need to specialize two variables, which definitely doesn't have to cause circular dependencies.

optional -> __format/range_default_formatter.h -> __format/range_formatter.h -> __format/format_context.h -> optional
^ What I need from __format/range_default_formatter.h is std::range_format.

I absolutely agree that this isn't preferable. I'll keep this open for now and look for another way to get the std::range_format. Now that I post this, I think we can just cut that enum out into its own header....

But here's an example of where it's going wrong:

# | In file included from /home/willtv/smallpop-llvm-project/libcxx/test/libcxx/utilities/optional/optional.object/iterator.pass.cpp:23:
# | In file included from /home/willtv/smallpop-llvm-project/build-optional/libcxx/test-suite-install/include/c++/v1/optional:245:
# | In file included from /home/willtv/smallpop-llvm-project/build-optional/libcxx/test-suite-install/include/c++/v1/__format/range_default_formatter.h:23:
# | In file included from /home/willtv/smallpop-llvm-project/build-optional/libcxx/test-suite-install/include/c++/v1/__format/range_formatter.h:23:
# | /home/willtv/smallpop-llvm-project/build-optional/libcxx/test-suite-install/include/c++/v1/__format/format_context.h:57:25: error: no template named 'optional'
# |    57 |                         optional<std::locale>&& __loc = nullopt) {
# |       |                         ^
# | /home/willtv/smallpop-llvm-project/build-optional/libcxx/test-suite-install/include/c++/v1/__format/format_context.h:57:57: error: use of undeclared identifier 'nullopt'; did you mean 'nullptr'?
# |    57 |                         optional<std::locale>&& __loc = nullopt) {
# |       |                                                         ^~~~~~~
# |       |                                                         nullptr
# | /home/willtv/smallpop-llvm-project/build-optional/libcxx/test-suite-install/include/c++/v1/__format/format_context.h:57:57: error: address of overloaded function 'nullopt' does not match required type 'int'
# |    57 |                         optional<std::locale>&& __loc = nullopt) {
# |       |                                                         ^~~~~~~
# | /home/willtv/smallpop-llvm-project/build-optional/libcxx/test-suite-install/include/c++/v1/__format/format_context.h:57:49: note: passing argument to parameter '__loc' here
# |    57 |                         optional<std::locale>&& __loc = nullopt) {
# |       |                                                 ^
# | /home/willtv/smallpop-llvm-project/build-optional/libcxx/test-suite-install/include/c++/v1/__format/format_context.h:110:3: error: no template named 'optional'
# |   110 |   optional<std::locale> __loc_;
# |       |   ^
# | /home/willtv/smallpop-llvm-project/build-optional/libcxx/test-suite-install/include/c++/v1/__format/format_context.h:114:87: error: no template named 'optional'
# |   114 |       _OtherOutIt, basic_format_args<basic_format_context<_OtherOutIt, _OtherCharT>>, optional<std::locale>&&);
# |       |                                                                                       ^
# | /home/willtv/smallpop-llvm-project/build-optional/libcxx/test-suite-install/include/c++/v1/__format/format_context.h:118:72: error: no template named 'optional'
# |   118 |       _OutIt __out_it, basic_format_args<basic_format_context> __args, optional<std::locale>&& __loc)
# |       |                                                                        ^
# | 6 errors generated.
# `-----------------------------
# error: command failed with exit status: 1```

@ldionne
Copy link
Member

ldionne commented Jul 15, 2025

IMO, what we should do is granularize <optional>:

  • <__optional/optional.h> contains the definition for std::optional
  • <__optional/format.h> contains the formatting-related stuff for std::optional
  • <optional> includes both

Then, from most places where we don't need std::format support, we'd just include <__optional/optional.h>. End-users would (obviously) include <optional> and they'd get everything they need.

I think that would break the cycle you encounter above because now __format/format_context.h would include <__optional/optional.h>, which doesn't try to re-include formatters. What do you think?

@philnik777
Copy link
Contributor

If we just need range_format then I think we should just include that in <optional> If we do need to include more formatter stuff then I agree we should granularize <optional>.

@smallp-o-p smallp-o-p force-pushed the remove_optional_from_format_context branch from c193ffa to c897f1d Compare July 15, 2025 17:26
@smallp-o-p smallp-o-p changed the title [libc++] Replace std::optional in __format/format_context.h with a re-implementation [libc++] Granularize range_format and format_kind declarations Jul 15, 2025
@philnik777 philnik777 merged commit 2194bca into llvm:main Jul 17, 2025
74 of 77 checks passed
@llvm-ci
Copy link
Collaborator

llvm-ci commented Jul 17, 2025

LLVM Buildbot has detected a new failure on builder sanitizer-x86_64-linux-android running on sanitizer-buildbot-android while building libcxx at step 2 "annotate".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/186/builds/10797

Here is the relevant piece of the build log for the reference
Step 2 (annotate) failure: 'python ../sanitizer_buildbot/sanitizers/zorg/buildbot/builders/sanitizers/buildbot_selector.py' (failure)
...
[       OK ] AddressSanitizer.AtoiAndFriendsOOBTest (2320 ms)
[ RUN      ] AddressSanitizer.HasFeatureAddressSanitizerTest
[       OK ] AddressSanitizer.HasFeatureAddressSanitizerTest (5 ms)
[ RUN      ] AddressSanitizer.CallocReturnsZeroMem
[       OK ] AddressSanitizer.CallocReturnsZeroMem (14 ms)
[ DISABLED ] AddressSanitizer.DISABLED_TSDTest
[ RUN      ] AddressSanitizer.IgnoreTest
[       OK ] AddressSanitizer.IgnoreTest (0 ms)
[ RUN      ] AddressSanitizer.SignalTest
[       OK ] AddressSanitizer.SignalTest (203 ms)
[ RUN      ] AddressSanitizer.ReallocTest
[       OK ] AddressSanitizer.ReallocTest (35 ms)
[ RUN      ] AddressSanitizer.WrongFreeTest
[       OK ] AddressSanitizer.WrongFreeTest (118 ms)
[ RUN      ] AddressSanitizer.LongJmpTest
[       OK ] AddressSanitizer.LongJmpTest (0 ms)
[ RUN      ] AddressSanitizer.ThreadStackReuseTest
[       OK ] AddressSanitizer.ThreadStackReuseTest (12 ms)
[ DISABLED ] AddressSanitizer.DISABLED_MemIntrinsicUnalignedAccessTest
[ DISABLED ] AddressSanitizer.DISABLED_LargeFunctionSymbolizeTest
[ DISABLED ] AddressSanitizer.DISABLED_MallocFreeUnwindAndSymbolizeTest
[ RUN      ] AddressSanitizer.UseThenFreeThenUseTest
[       OK ] AddressSanitizer.UseThenFreeThenUseTest (98 ms)
[ RUN      ] AddressSanitizer.FileNameInGlobalReportTest
[       OK ] AddressSanitizer.FileNameInGlobalReportTest (128 ms)
[ DISABLED ] AddressSanitizer.DISABLED_StressStackReuseAndExceptionsTest
[ RUN      ] AddressSanitizer.MlockTest
[       OK ] AddressSanitizer.MlockTest (0 ms)
[ DISABLED ] AddressSanitizer.DISABLED_DemoThreadedTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoStackTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoThreadStackTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoUAFLowIn
[ DISABLED ] AddressSanitizer.DISABLED_DemoUAFLowLeft
[ DISABLED ] AddressSanitizer.DISABLED_DemoUAFLowRight
[ DISABLED ] AddressSanitizer.DISABLED_DemoUAFHigh
[ DISABLED ] AddressSanitizer.DISABLED_DemoOOM
[ DISABLED ] AddressSanitizer.DISABLED_DemoDoubleFreeTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoNullDerefTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoFunctionStaticTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoTooMuchMemoryTest
[ RUN      ] AddressSanitizer.LongDoubleNegativeTest
[       OK ] AddressSanitizer.LongDoubleNegativeTest (0 ms)
[----------] 19 tests from AddressSanitizer (28039 ms total)

[----------] Global test environment tear-down
[==========] 22 tests from 2 test suites ran. (28050 ms total)
[  PASSED  ] 22 tests.

  YOU HAVE 1 DISABLED TEST

Step 34 (run instrumented asan tests [aarch64/bluejay-userdebug/TQ3A.230805.001]) failure: run instrumented asan tests [aarch64/bluejay-userdebug/TQ3A.230805.001] (failure)
...
[ RUN      ] AddressSanitizer.HasFeatureAddressSanitizerTest
[       OK ] AddressSanitizer.HasFeatureAddressSanitizerTest (5 ms)
[ RUN      ] AddressSanitizer.CallocReturnsZeroMem
[       OK ] AddressSanitizer.CallocReturnsZeroMem (14 ms)
[ DISABLED ] AddressSanitizer.DISABLED_TSDTest
[ RUN      ] AddressSanitizer.IgnoreTest
[       OK ] AddressSanitizer.IgnoreTest (0 ms)
[ RUN      ] AddressSanitizer.SignalTest
[       OK ] AddressSanitizer.SignalTest (203 ms)
[ RUN      ] AddressSanitizer.ReallocTest
[       OK ] AddressSanitizer.ReallocTest (35 ms)
[ RUN      ] AddressSanitizer.WrongFreeTest
[       OK ] AddressSanitizer.WrongFreeTest (118 ms)
[ RUN      ] AddressSanitizer.LongJmpTest
[       OK ] AddressSanitizer.LongJmpTest (0 ms)
[ RUN      ] AddressSanitizer.ThreadStackReuseTest
[       OK ] AddressSanitizer.ThreadStackReuseTest (12 ms)
[ DISABLED ] AddressSanitizer.DISABLED_MemIntrinsicUnalignedAccessTest
[ DISABLED ] AddressSanitizer.DISABLED_LargeFunctionSymbolizeTest
[ DISABLED ] AddressSanitizer.DISABLED_MallocFreeUnwindAndSymbolizeTest
[ RUN      ] AddressSanitizer.UseThenFreeThenUseTest
[       OK ] AddressSanitizer.UseThenFreeThenUseTest (98 ms)
[ RUN      ] AddressSanitizer.FileNameInGlobalReportTest
[       OK ] AddressSanitizer.FileNameInGlobalReportTest (128 ms)
[ DISABLED ] AddressSanitizer.DISABLED_StressStackReuseAndExceptionsTest
[ RUN      ] AddressSanitizer.MlockTest
[       OK ] AddressSanitizer.MlockTest (0 ms)
[ DISABLED ] AddressSanitizer.DISABLED_DemoThreadedTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoStackTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoThreadStackTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoUAFLowIn
[ DISABLED ] AddressSanitizer.DISABLED_DemoUAFLowLeft
[ DISABLED ] AddressSanitizer.DISABLED_DemoUAFLowRight
[ DISABLED ] AddressSanitizer.DISABLED_DemoUAFHigh
[ DISABLED ] AddressSanitizer.DISABLED_DemoOOM
[ DISABLED ] AddressSanitizer.DISABLED_DemoDoubleFreeTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoNullDerefTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoFunctionStaticTest
[ DISABLED ] AddressSanitizer.DISABLED_DemoTooMuchMemoryTest
[ RUN      ] AddressSanitizer.LongDoubleNegativeTest
[       OK ] AddressSanitizer.LongDoubleNegativeTest (0 ms)
[----------] 19 tests from AddressSanitizer (28039 ms total)

[----------] Global test environment tear-down
[==========] 22 tests from 2 test suites ran. (28050 ms total)
[  PASSED  ] 22 tests.

  YOU HAVE 1 DISABLED TEST
program finished with exit code 0
elapsedTime=2242.441928

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants