Skip to content

Conversation

@smallp-o-p
Copy link
Contributor

@smallp-o-p smallp-o-p commented Aug 25, 2025

Resolves #148131

  • Unlock std::optional<T&> implementation
  • Allow instantiations of optional<T(&)(...)> and optional<T(&)[]> but disables value_or() and optional::iterator + all iterator related functions
  • Update documentation
  • Update tests

@smallp-o-p smallp-o-p requested a review from a team as a code owner August 25, 2025 04:23
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Aug 25, 2025
@llvmbot
Copy link
Member

llvmbot commented Aug 25, 2025

@llvm/pr-subscribers-libcxx

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

Changes

Resolves #148131

  • Unlock std::optional&lt;T&amp;&gt; implementation, implementation allows optional&lt;T(&amp;)(...)&gt; and optional&lt;T(&amp;)[]&gt; to be instantiated but disables value_or() and optional::iterator (and all iterator related functions)
  • Update documentation
  • Update tests

Patch is 44.80 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/155202.diff

29 Files Affected:

  • (modified) libcxx/docs/FeatureTestMacroTable.rst (+2)
  • (modified) libcxx/docs/ReleaseNotes/22.rst (+1)
  • (modified) libcxx/docs/Status/Cxx2cPapers.csv (+1-1)
  • (modified) libcxx/include/__iterator/wrap_iter.h (+2-2)
  • (modified) libcxx/include/optional (+83-47)
  • (modified) libcxx/test/libcxx/utilities/optional/optional.iterator/iterator.compile.pass.cpp (+4-5)
  • (added) libcxx/test/libcxx/utilities/optional/optional.object/optional.object.observe/value_or.compile.pass.cpp (+27)
  • (modified) libcxx/test/std/utilities/optional/optional.iterator/begin.pass.cpp (+6-1)
  • (modified) libcxx/test/std/utilities/optional/optional.iterator/end.pass.cpp (+7-1)
  • (modified) libcxx/test/std/utilities/optional/optional.iterator/iterator.pass.cpp (+17-10)
  • (modified) libcxx/test/std/utilities/optional/optional.monadic/and_then.pass.cpp (+80)
  • (modified) libcxx/test/std/utilities/optional/optional.monadic/or_else.pass.cpp (+26)
  • (modified) libcxx/test/std/utilities/optional/optional.monadic/transform.pass.cpp (+115-13)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.assign/assign_value.pass.cpp (+35-1)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.assign/emplace.pass.cpp (+24-1)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.ctor/ctor.verify.cpp (+4)
  • (added) libcxx/test/std/utilities/optional/optional.object/optional.object.ctor/ref_t.pass.cpp (+40)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.dtor/dtor.pass.cpp (+8-2)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.mod/reset.pass.cpp (+11)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.observe/dereference.pass.cpp (+13-1)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.observe/dereference_const.pass.cpp (+19)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.observe/has_value.pass.cpp (+8-1)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.observe/op_arrow.pass.cpp (+25)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.observe/op_arrow_const.pass.cpp (+19)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.observe/value.pass.cpp (+8)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.observe/value_or.pass.cpp (+8)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional.object.observe/value_or_const.pass.cpp (+7-1)
  • (modified) libcxx/test/std/utilities/optional/optional.object/optional_requires_destructible_object.verify.cpp (+10-3)
  • (modified) libcxx/utils/generate_feature_test_macro_components.py (+1)
diff --git a/libcxx/docs/FeatureTestMacroTable.rst b/libcxx/docs/FeatureTestMacroTable.rst
index 358889d8dbc37..c10fc4c365407 100644
--- a/libcxx/docs/FeatureTestMacroTable.rst
+++ b/libcxx/docs/FeatureTestMacroTable.rst
@@ -480,6 +480,8 @@ Status
     ---------------------------------------------------------- -----------------
     ``__cpp_lib_not_fn``                                       ``202306L``
     ---------------------------------------------------------- -----------------
+    ``__cpp_lib_optional``                                     ``202506L``
+    ---------------------------------------------------------- -----------------
     ``__cpp_lib_optional_range_support``                       ``202406L``
     ---------------------------------------------------------- -----------------
     ``__cpp_lib_out_ptr``                                      ``202311L``
diff --git a/libcxx/docs/ReleaseNotes/22.rst b/libcxx/docs/ReleaseNotes/22.rst
index a26c5476d421b..e070169c87ba3 100644
--- a/libcxx/docs/ReleaseNotes/22.rst
+++ b/libcxx/docs/ReleaseNotes/22.rst
@@ -39,6 +39,7 @@ Implemented Papers
 ------------------
 
 - P2321R2: ``zip`` (`Github <https://github.com/llvm/llvm-project/issues/105169>`__) (The paper is partially implemented. ``zip_transform_view`` is implemented in this release)
+- P2988R12: ``std::optional<T&>`` (`Github <https://github.com/llvm/llvm-project/issues/148131>`__)
 - P3168R2: Give ``std::optional`` Range Support (`Github <https://github.com/llvm/llvm-project/issues/105430>`__)
 
 Improvements and New Features
diff --git a/libcxx/docs/Status/Cxx2cPapers.csv b/libcxx/docs/Status/Cxx2cPapers.csv
index 3b8b2b7ad0b3f..5e28875c26944 100644
--- a/libcxx/docs/Status/Cxx2cPapers.csv
+++ b/libcxx/docs/Status/Cxx2cPapers.csv
@@ -122,7 +122,7 @@
 "`P3293R3 <https://wg21.link/P3293R3>`__","Splicing a base class subobject","2025-06 (Sofia)","","",""
 "`P3491R3 <https://wg21.link/P3491R3>`__","``define_static_{string,object,array}``","2025-06 (Sofia)","","",""
 "`P3096R12 <https://wg21.link/P3096R12>`__","Function Parameter Reflection in Reflection for C++26","2025-06 (Sofia)","","",""
-"`P2988R12 <https://wg21.link/P2988R12>`__","``std::optional<T&>``","2025-06 (Sofia)","","",""
+"`P2988R12 <https://wg21.link/P2988R12>`__","``std::optional<T&>``","2025-06 (Sofia)","|Complete|","22",""
 "`P3348R4 <https://wg21.link/P3348R4>`__","C++26 should refer to C23 not C17","2025-06 (Sofia)","","",""
 "`P3037R6 <https://wg21.link/P3037R6>`__","``constexpr`` ``std::shared_ptr`` and friends","2025-06 (Sofia)","","",""
 "`P3284R4 <https://wg21.link/P3284R4>`__","``write_env`` and ``unstoppable`` Sender Adaptors","2025-06 (Sofia)","","",""
diff --git a/libcxx/include/__iterator/wrap_iter.h b/libcxx/include/__iterator/wrap_iter.h
index 7610586ddecbb..8e5c7388812d3 100644
--- a/libcxx/include/__iterator/wrap_iter.h
+++ b/libcxx/include/__iterator/wrap_iter.h
@@ -117,8 +117,8 @@ class __wrap_iter {
   friend class span;
   template <class _Tp, size_t _Size>
   friend struct array;
-  template <class _Tp>
-  friend class optional;
+  template <class _Tp, class>
+  friend struct __optional_iterator;
 };
 
 template <class _Iter1>
diff --git a/libcxx/include/optional b/libcxx/include/optional
index 39fcaa2c2ec18..489691f33e0cd 100644
--- a/libcxx/include/optional
+++ b/libcxx/include/optional
@@ -161,8 +161,8 @@ namespace std {
     constexpr T &value() &;
     constexpr T &&value() &&;
     constexpr const T &&value() const &&;
-    template<class U> constexpr T value_or(U &&) const &;
-    template<class U> constexpr T value_or(U &&) &&;
+    template<class U = remove_cv_t<T>> constexpr T value_or(U&& v) const &;
+    template<class U = remove_cv_t<T>> constexpr T value_or(U&& v) &&;
 
     // [optional.monadic], monadic operations
     template<class F> constexpr auto and_then(F&& f) &;         // since C++23
@@ -412,9 +412,6 @@ struct __optional_storage_base : __optional_destruct_base<_Tp> {
   }
 };
 
-// optional<T&> is currently required to be ill-formed. However, it may
-// be allowed in the future. For this reason, it has already been implemented
-// to ensure we can make the change in an ABI-compatible manner.
 template <class _Tp>
 struct __optional_storage_base<_Tp, true> {
   using value_type                 = _Tp;
@@ -607,19 +604,19 @@ struct __is_std_optional : false_type {};
 template <class _Tp>
 struct __is_std_optional<optional<_Tp>> : true_type {};
 
-template <class _Tp>
-class _LIBCPP_DECLSPEC_EMPTY_BASES optional
-    : private __optional_move_assign_base<_Tp>,
-      private __optional_sfinae_ctor_base_t<_Tp>,
-      private __optional_sfinae_assign_base_t<_Tp> {
-  using __base _LIBCPP_NODEBUG = __optional_move_assign_base<_Tp>;
+template <class _Tp, class = void>
+struct __optional_iterator {};
 
-  using __pointer _LIBCPP_NODEBUG       = std::add_pointer_t<_Tp>;
-  using __const_pointer _LIBCPP_NODEBUG = std::add_pointer_t<const _Tp>;
+template <class _Tp>
+struct __optional_iterator<
+    _Tp,
+    enable_if_t<!(is_lvalue_reference_v<_Tp> && is_function_v<__libcpp_remove_reference_t<_Tp>>) &&
+                !(is_lvalue_reference_v<_Tp> && is_array_v<__libcpp_remove_reference_t<_Tp>>)> > {
+private:
+  using __pointer _LIBCPP_NODEBUG       = std::add_pointer_t<remove_reference_t<_Tp>>;
+  using __const_pointer _LIBCPP_NODEBUG = std::add_pointer_t<const remove_reference_t<_Tp>>;
 
 public:
-  using value_type = _Tp;
-
 #    if _LIBCPP_STD_VER >= 26
 #      ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_OPTIONAL
   using iterator       = __bounded_iter<__wrap_iter<__pointer>>;
@@ -628,18 +625,67 @@ public:
   using iterator       = __wrap_iter<__pointer>;
   using const_iterator = __wrap_iter<__const_pointer>;
 #      endif
+
+  // [optional.iterators], iterator support
+  _LIBCPP_HIDE_FROM_ABI constexpr iterator begin() noexcept {
+    auto& __derived_self = static_cast<optional<_Tp>&>(*this);
+#      ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_OPTIONAL
+    return std::__make_bounded_iter(
+        std::__wrap_iter<__pointer>(std::addressof(__derived_self.__get())),
+        std::__wrap_iter<__pointer>(std::addressof(__derived_self.__get())),
+        std::__wrap_iter<__pointer>(std::addressof(__derived_self.__get()) + (__derived_self.has_value() ? 1 : 0)));
+#      else
+    return iterator(std::addressof(__derived_self.__get()));
+#      endif
+  }
+
+  _LIBCPP_HIDE_FROM_ABI constexpr const_iterator begin() const noexcept {
+    auto& __derived_self = static_cast<const optional<_Tp>&>(*this);
+#      ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_OPTIONAL
+    return std::__make_bounded_iter(
+        std::__wrap_iter<__const_pointer>(std::addressof(__derived_self.__get())),
+        std::__wrap_iter<__const_pointer>(std::addressof(__derived_self.__get())),
+        std::__wrap_iter<__const_pointer>(
+            std::addressof(__derived_self.__get()) + (__derived_self.has_value() ? 1 : 0)));
+#      else
+    return const_iterator(std::addressof(__derived_self.__get()));
+#      endif
+  }
+
+  _LIBCPP_HIDE_FROM_ABI constexpr iterator end() noexcept {
+    return begin() + (static_cast<optional<_Tp>&>(*this).has_value() ? 1 : 0);
+  }
+  _LIBCPP_HIDE_FROM_ABI constexpr const_iterator end() const noexcept {
+    return begin() + (static_cast<const optional<_Tp>&>(*this).has_value() ? 1 : 0);
+  }
 #    endif
+};
+
+template <class _Tp>
+class _LIBCPP_DECLSPEC_EMPTY_BASES optional
+    : private __optional_move_assign_base<_Tp>,
+      private __optional_sfinae_ctor_base_t<_Tp>,
+      private __optional_sfinae_assign_base_t<_Tp>,
+      public __optional_iterator<_Tp> {
+  using __base _LIBCPP_NODEBUG = __optional_move_assign_base<_Tp>;
+
+public:
+  using value_type = _Tp;
+
   using __trivially_relocatable _LIBCPP_NODEBUG =
       conditional_t<__libcpp_is_trivially_relocatable<_Tp>::value, optional, void>;
   using __replaceable _LIBCPP_NODEBUG = conditional_t<__is_replaceable_v<_Tp>, optional, void>;
 
 private:
-  // Disable the reference extension using this static assert.
   static_assert(!is_same_v<__remove_cvref_t<value_type>, in_place_t>,
                 "instantiation of optional with in_place_t is ill-formed");
   static_assert(!is_same_v<__remove_cvref_t<value_type>, nullopt_t>,
                 "instantiation of optional with nullopt_t is ill-formed");
-  static_assert(!is_reference_v<value_type>, "instantiation of optional with a reference type is ill-formed");
+#    if _LIBCPP_STD_VER >= 26
+  static_assert(!is_rvalue_reference_v<_Tp>, "instantiation of optional with an rvalue reference type is ill-formed");
+#    else
+  static_assert(!is_reference_v<_Tp>, "instantiation of optional with a reference type is ill-formed");
+#    endif
   static_assert(is_destructible_v<value_type>, "instantiation of optional with a non-destructible type is ill-formed");
   static_assert(!is_array_v<value_type>, "instantiation of optional with an array type is ill-formed");
 
@@ -832,34 +878,6 @@ public:
     }
   }
 
-#    if _LIBCPP_STD_VER >= 26
-  // [optional.iterators], iterator support
-  _LIBCPP_HIDE_FROM_ABI constexpr iterator begin() noexcept {
-#      ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_OPTIONAL
-    return std::__make_bounded_iter(
-        std::__wrap_iter<__pointer>(std::addressof(this->__get())),
-        std::__wrap_iter<__pointer>(std::addressof(this->__get())),
-        std::__wrap_iter<__pointer>(std::addressof(this->__get()) + (this->has_value() ? 1 : 0)));
-#      else
-    return iterator(std::addressof(this->__get()));
-#      endif
-  }
-
-  _LIBCPP_HIDE_FROM_ABI constexpr const_iterator begin() const noexcept {
-#      ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_OPTIONAL
-    return std::__make_bounded_iter(
-        std::__wrap_iter<__const_pointer>(std::addressof(this->__get())),
-        std::__wrap_iter<__const_pointer>(std::addressof(this->__get())),
-        std::__wrap_iter<__const_pointer>(std::addressof(this->__get()) + (this->has_value() ? 1 : 0)));
-#      else
-    return const_iterator(std::addressof(this->__get()));
-#      endif
-  }
-
-  _LIBCPP_HIDE_FROM_ABI constexpr iterator end() noexcept { return begin() + (this->has_value() ? 1 : 0); }
-  _LIBCPP_HIDE_FROM_ABI constexpr const_iterator end() const noexcept { return begin() + (this->has_value() ? 1 : 0); }
-#    endif
-
   _LIBCPP_HIDE_FROM_ABI constexpr add_pointer_t<value_type const> operator->() const noexcept {
     _LIBCPP_ASSERT_VALID_ELEMENT_ACCESS(this->has_value(), "optional operator-> called on a disengaged value");
     return std::addressof(this->__get());
@@ -919,20 +937,38 @@ public:
     return std::move(this->__get());
   }
 
-  template <class _Up>
+  template <class _Up = remove_cv_t<_Tp>>
+#    if _LIBCPP_STD_VER >= 26
+    requires(!(is_lvalue_reference_v<_Tp> && is_function_v<__libcpp_remove_reference_t<_Tp>>) &&
+             !(is_lvalue_reference_v<_Tp> && is_array_v<__libcpp_remove_reference_t<_Tp>>))
+#    endif
   _LIBCPP_HIDE_FROM_ABI constexpr value_type value_or(_Up&& __v) const& {
     static_assert(is_copy_constructible_v<value_type>, "optional<T>::value_or: T must be copy constructible");
     static_assert(is_convertible_v<_Up, value_type>, "optional<T>::value_or: U must be convertible to T");
     return this->has_value() ? this->__get() : static_cast<value_type>(std::forward<_Up>(__v));
   }
 
-  template <class _Up>
+  template <class _Up = remove_cv_t<_Tp>>
+#    if _LIBCPP_STD_VER >= 26
+    requires(!is_lvalue_reference_v<_Tp>)
+#    endif
   _LIBCPP_HIDE_FROM_ABI constexpr value_type value_or(_Up&& __v) && {
     static_assert(is_move_constructible_v<value_type>, "optional<T>::value_or: T must be move constructible");
     static_assert(is_convertible_v<_Up, value_type>, "optional<T>::value_or: U must be convertible to T");
     return this->has_value() ? std::move(this->__get()) : static_cast<value_type>(std::forward<_Up>(__v));
   }
 
+#    if _LIBCPP_STD_VER >= 26
+  template <class _Up = remove_cv_t<_Tp>>
+    requires(is_lvalue_reference_v<_Tp> &&
+             !(is_function_v<__libcpp_remove_reference_t<_Tp>> || is_array_v<__libcpp_remove_reference_t<_Tp>>))
+  _LIBCPP_HIDE_FROM_ABI constexpr value_type value_or(_Up&& __v) && {
+    static_assert(is_move_constructible_v<value_type>, "optional<T>::value_or: T must be move constructible");
+    static_assert(is_convertible_v<_Up, value_type>, "optional<T>::value_or: U must be convertible to T");
+    return this->has_value() ? this->__get() : static_cast<value_type>(std::forward<_Up>(__v));
+  }
+#    endif
+
 #    if _LIBCPP_STD_VER >= 23
   template <class _Func>
   _LIBCPP_HIDE_FROM_ABI constexpr auto and_then(_Func&& __f) & {
diff --git a/libcxx/test/libcxx/utilities/optional/optional.iterator/iterator.compile.pass.cpp b/libcxx/test/libcxx/utilities/optional/optional.iterator/iterator.compile.pass.cpp
index 3cdd7553e2e5d..472049a91a8d1 100644
--- a/libcxx/test/libcxx/utilities/optional/optional.iterator/iterator.compile.pass.cpp
+++ b/libcxx/test/libcxx/utilities/optional/optional.iterator/iterator.compile.pass.cpp
@@ -23,8 +23,7 @@ concept has_iterator_aliases = requires {
 
 static_assert(has_iterator_aliases<std::optional<int>>);
 static_assert(has_iterator_aliases<std::optional<const int>>);
-
-// TODO: Uncomment these once P2988R12 is implemented, as they would be testing optional<T&>
-
-// static_assert(!has_iterator_aliases<std::optional<int (&)[]>>);
-// static_assert(!has_iterator_aliases<std::optional<void (&)(int, char)>>);
+static_assert(has_iterator_aliases<std::optional<int&>>);
+static_assert(has_iterator_aliases<std::optional<const int&>>);
+static_assert(!has_iterator_aliases<std::optional<int(&)[1]>>);
+static_assert(!has_iterator_aliases<std::optional<int(&)()>>);
diff --git a/libcxx/test/libcxx/utilities/optional/optional.object/optional.object.observe/value_or.compile.pass.cpp b/libcxx/test/libcxx/utilities/optional/optional.object/optional.object.observe/value_or.compile.pass.cpp
new file mode 100644
index 0000000000000..298e2f6f55d4d
--- /dev/null
+++ b/libcxx/test/libcxx/utilities/optional/optional.object/optional.object.observe/value_or.compile.pass.cpp
@@ -0,0 +1,27 @@
+
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+
+// <optional>
+
+// template <class U> T optional<T>::value_or(U&&);
+
+#include <optional>
+
+template <typename Opt, typename T>
+concept has_value_or = requires(Opt opt, T&& t) {
+  { opt.value_or(t) } -> std::same_as<T>;
+};
+
+static_assert(has_value_or<std::optional<int>, int>);
+static_assert(has_value_or<std::optional<int&>, int&>);
+static_assert(has_value_or<std::optional<const int&>, const int&>);
+static_assert(!has_value_or<std::optional<int(&)[1]>&&, int(&)[1]>);
+static_assert(!has_value_or<std::optional<int(&)()>&&, int(&)()>);
diff --git a/libcxx/test/std/utilities/optional/optional.iterator/begin.pass.cpp b/libcxx/test/std/utilities/optional/optional.iterator/begin.pass.cpp
index df95a8df3793f..81234525923a1 100644
--- a/libcxx/test/std/utilities/optional/optional.iterator/begin.pass.cpp
+++ b/libcxx/test/std/utilities/optional/optional.iterator/begin.pass.cpp
@@ -21,7 +21,8 @@
 
 template <typename T>
 constexpr bool test() {
-  std::optional<T> opt{T{}};
+  std::remove_reference_t<T> t = std::remove_reference_t<T>{};
+  std::optional<T> opt{t};
 
   { // begin() is marked noexcept
     static_assert(noexcept(opt.begin()));
@@ -53,6 +54,10 @@ constexpr bool tests() {
   assert(test<char>());
   assert(test<const int>());
   assert(test<const char>());
+  assert(test<int&>());
+  assert(test<char&>());
+  assert(test<const int&>());
+  assert(test<const char&>());
   return true;
 }
 
diff --git a/libcxx/test/std/utilities/optional/optional.iterator/end.pass.cpp b/libcxx/test/std/utilities/optional/optional.iterator/end.pass.cpp
index 966c3e7441880..c62c9fc7746d6 100644
--- a/libcxx/test/std/utilities/optional/optional.iterator/end.pass.cpp
+++ b/libcxx/test/std/utilities/optional/optional.iterator/end.pass.cpp
@@ -17,6 +17,7 @@
 #include <iterator>
 #include <optional>
 #include <ranges>
+#include <type_traits>
 #include <utility>
 
 template <typename T>
@@ -41,7 +42,8 @@ constexpr bool test() {
     assert(it2 == std::as_const(disengaged).end());
   }
 
-  std::optional<T> engaged{T{}};
+  std::remove_reference_t<T> t = std::remove_reference_t<T>{};
+  std::optional<T> engaged{t};
 
   { // end() != begin() if the optional is engaged
     auto it  = engaged.end();
@@ -62,6 +64,10 @@ constexpr bool tests() {
   assert(test<char>());
   assert(test<const int>());
   assert(test<const char>());
+  assert(test<int&>());
+  assert(test<char&>());
+  assert(test<const int&>());
+  assert(test<const char&>());
 
   return true;
 }
diff --git a/libcxx/test/std/utilities/optional/optional.iterator/iterator.pass.cpp b/libcxx/test/std/utilities/optional/optional.iterator/iterator.pass.cpp
index 1203290a0290a..34d0d6b135564 100644
--- a/libcxx/test/std/utilities/optional/optional.iterator/iterator.pass.cpp
+++ b/libcxx/test/std/utilities/optional/optional.iterator/iterator.pass.cpp
@@ -20,9 +20,10 @@
 #include <type_traits>
 #include <utility>
 
-template <typename T, T __val>
+template <typename T, std::remove_reference_t<T> __val>
 constexpr bool test() {
-  std::optional<T> opt{__val};
+  std::remove_reference_t<T> v{__val};
+  std::optional<T> opt{v};
 
   { // Dereferencing an iterator of an engaged optional will return the same value that the optional holds.
     auto it  = opt.begin();
@@ -41,13 +42,14 @@ constexpr bool test() {
     assert(std::random_access_iterator<decltype(it2)>);
   }
 
-  { // const_iterator::value_type == std::remove_cv_t<T>, const_iterator::reference == const T&, iterator::value_type = std::remove_cv_t<T>, iterator::reference == T&
+  { // const_iterator::value_type == std::remove_cvref_t<T>, const_iterator::reference == const T&, iterator::value_type = std::remove_cvref_t<T>, iterator::reference == T&
+    // std::remove_cv_t is impossible for optional<T&>
     auto it  = opt.begin();
     auto it2 = std::as_const(opt).begin();
-    assert((std::is_same_v<typename decltype(it)::value_type, std::remove_cv_t<T>>));
-    assert((std::is_same_v<typename decltype(it)::reference, T&>));
-    assert((std::is_same_v<typename decltype(it2)::value_type, std::remove_cv_t<T>>));
-    assert((std::is_same_v<typename decltype(it2)::reference, const T&>));
+    assert((std::is_same_v<typename decltype(it)::value_type, std::remove_cvref_t<T>>));
+    assert((std::is_same_v<typename decltype(it)::reference, std::remove_reference_t<T>&>));
+    assert((std::is_same_v<typename decltype(it2)::value_type, std::remove_cvref_t<T>>));
+    assert((std::is_same_v<typename decltype(it2)::reference, const std::remove_reference_t<T>&>));
   }
 
   { // std::ranges::size for an engaged optional<T> == 1, disengaged optional<T> == 0
@@ -68,13 +70,13 @@ constexpr bool test() {
   // An optional with value that is reset will have a begin() == end(), then when it is reassigned a value,
   // begin() != end(), and *begin() will contain the new value.
   {
-    std::optional<T> val{__val};
+    std::optional<T> val{v};
     assert(val.begin() != val.end());
     val.reset();
     assert(val.begin() == val.end());
-    val.emplace(__val);
+    val.emplace(v);
     assert(val.begin() != val.end());
-    assert(*(val.begin()) == __val);
+    assert(*(val.begin()) == v);
   }
 
   return true;
@@ -86,6 +88,11 @@ constexpr bool tests() {
   assert((test<bool, true>()));
   assert((test<const int, 2>()));
   assert((test<const char, 'b'>()));
+  assert((test<int&, 1>()));
+  assert((test<char&, 'a'>()));
+  assert((test<bool&, true>()));
+  assert((test<const int&, 2>()));
+  assert((test<const char&, 'b'>()));
 
   return true;
 }
diff --git a/libcxx/test/std/utilities/optional/optional.monadic/and_then.pass.cpp b/libcxx/test/std/utilities/optional/optional.monadic/and_then.pass.cpp
index 97305d976e066..3d945e437151e 100644
--- a/libcxx/test/std/utilities/optional/optional.monadic/and_then.pass.cpp
+++ b/libcxx/test/std/utilities/optional/optional.monadic/and_then.pass.cpp
@@ -257,8 +257,88 @@ c...
[truncated]

@github-actions
Copy link

github-actions bot commented Aug 25, 2025

✅ With the latest revision this PR passed the Python code formatter.

@github-actions
Copy link

github-actions bot commented Aug 25, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@smallp-o-p smallp-o-p marked this pull request as draft August 25, 2025 04:43
@smallp-o-p smallp-o-p changed the title [libc++] Implement P2988R12: `std::optional<T&> [libc++] Implement P2988R12: std::optional<T&> Aug 25, 2025
@smallp-o-p smallp-o-p force-pushed the implement-P2988R12 branch 2 times, most recently from a5e5e09 to c12481f Compare September 25, 2025 03:58
@smallp-o-p smallp-o-p marked this pull request as ready for review September 25, 2025 19:58
@smallp-o-p
Copy link
Contributor Author

Marking as ready + a reminder ping

@vogelsgesang
Copy link
Member

vogelsgesang commented Sep 26, 2025

afaict, the physical layout representation of a an optional<Foo&> would be

struct OptionalFooRef {
  union {
    char __null_state_;
    Foo& __val_;
  };
  bool __engaged_;
};

I think a more optimal representation would be

struct OptionalFooRef {
  // nullopt encoded as nullptr, since `nullptr` is not a valid reference
  Foo* __val_;
};

This would allow me to use optional<Foo&> as a more type-safe replacement for Foo* without any performance penalties.

Given that optional<Foo&> was previously forbidden, this wouldn't be an ABI break. However, if we would merge your PR as is, we could no longer do that optimization later on without breaking the ABI.

(Please note that I am not a contributor / reviewer for libc++. So please wait for feedback from others before acting on my comment. Not sure if I might be sending you into the wrong direction here 🙂)

@smallp-o-p
Copy link
Contributor Author

smallp-o-p commented Sep 26, 2025

afaict, the physical layout representation of a an optional<Foo&> would be

struct OptionalFooRef {

  union {

    char __null_state_;

    Foo& __val_;

  };

  bool __engaged_;

};

I think a more optimal representation would be

struct OptionalFooRef {

  // nullopt encoded as nullptr, since `nullptr` is not a valid reference

  Foo* __val_;

};

This would allow me to use optional<Foo&> as a more type-safe replacement for Foo* without any performance penalties.

Given that optional<Foo&> was previously forbidden, this wouldn't be an ABI break. However, if we would merge your PR as is, we could no longer do that optimization later on without breaking the ABI.

(Please note that I am not a contributor / reviewer for libc++. So please wait for feedback from others before acting on my comment. Not sure if I might be sending you into the wrong direction here 🙂)

The storage base for optional<T&> is implemented as you specify via a template specialization SFINAE, and it's actually been there for quite some time—I want to guess when optional was originally implemented?

Either way you'll be pleased to know the original implementers thought of this already 😄

@frederick-vs-ja
Copy link
Contributor

frederick-vs-ja commented Sep 26, 2025

I think a more optimal representation would be

struct OptionalFooRef {
  // nullopt encoded as nullptr, since `nullptr` is not a valid reference
  Foo* __val_;
};

Such a representation has been already implemented for years.

// optional<T&> is currently required to be ill-formed. However, it may
// be allowed in the future. For this reason, it has already been implemented
// to ensure we can make the change in an ABI-compatible manner.
template <class _Tp>
struct __optional_storage_base<_Tp, true> {
using value_type = _Tp;
using __raw_type _LIBCPP_NODEBUG = remove_reference_t<_Tp>;
__raw_type* __value_;
template <class _Up>
static _LIBCPP_HIDE_FROM_ABI constexpr bool __can_bind_reference() {
using _RawUp = __libcpp_remove_reference_t<_Up>;
using _UpPtr = _RawUp*;
using _RawTp = __libcpp_remove_reference_t<_Tp>;
using _TpPtr = _RawTp*;
using _CheckLValueArg =
integral_constant<bool,
(is_lvalue_reference<_Up>::value && is_convertible<_UpPtr, _TpPtr>::value) ||
is_same<_RawUp, reference_wrapper<_RawTp>>::value ||
is_same<_RawUp, reference_wrapper<__remove_const_t<_RawTp>>>::value >;
return (is_lvalue_reference<_Tp>::value && _CheckLValueArg::value) ||
(is_rvalue_reference<_Tp>::value && !is_lvalue_reference<_Up>::value &&
is_convertible<_UpPtr, _TpPtr>::value);
}
_LIBCPP_HIDE_FROM_ABI constexpr __optional_storage_base() noexcept : __value_(nullptr) {}
template <class _UArg>
_LIBCPP_HIDE_FROM_ABI constexpr explicit __optional_storage_base(in_place_t, _UArg&& __uarg)
: __value_(std::addressof(__uarg)) {
static_assert(__can_bind_reference<_UArg>(),
"Attempted to construct a reference element in tuple from a "
"possible temporary");
}
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 void reset() noexcept { __value_ = nullptr; }
_LIBCPP_HIDE_FROM_ABI constexpr bool has_value() const noexcept { return __value_ != nullptr; }
_LIBCPP_HIDE_FROM_ABI constexpr value_type& __get() const& noexcept { return *__value_; }
_LIBCPP_HIDE_FROM_ABI constexpr value_type&& __get() const&& noexcept { return std::forward<value_type>(*__value_); }
template <class _UArg>
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 void __construct(_UArg&& __val) {
_LIBCPP_ASSERT_INTERNAL(!has_value(), "__construct called for engaged __optional_storage");
static_assert(__can_bind_reference<_UArg>(),
"Attempted to construct a reference element in tuple from a "
"possible temporary");
__value_ = std::addressof(__val);
}
template <class _That>
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 void __construct_from(_That&& __opt) {
if (__opt.has_value())
__construct(std::forward<_That>(__opt).__get());
}
template <class _That>
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 void __assign_from(_That&& __opt) {
if (has_value() == __opt.has_value()) {
if (has_value())
*__value_ = std::forward<_That>(__opt).__get();
} else {
if (has_value())
reset();
else
__construct(std::forward<_That>(__opt).__get());
}
}
};

@smallp-o-p However, I'm not sure whether __can_bind_reference is still useful (or not harmful). Perhaps we should either remove or update it (with reference_constructs_from_temporary_v).

@smallp-o-p
Copy link
Contributor Author

Wonder what's up with the FreeBSD bot, seems like it can't find reference_constructs_from_temporary_v

@Zingam
Copy link
Contributor

Zingam commented Oct 1, 2025

Wonder what's up with the FreeBSD bot, seems like it can't find reference_constructs_from_temporary_v

It's on Clang 19. It should be updated fairly soon.

@smallp-o-p
Copy link
Contributor Author

Reminder ping

Copy link
Contributor

@frederick-vs-ja frederick-vs-ja left a comment

Choose a reason for hiding this comment

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

Looks mostly fine to me. Could you update the branch (together with addressing the review comments) to see whether CI passes?

Copy link
Contributor

@frederick-vs-ja frederick-vs-ja left a comment

Choose a reason for hiding this comment

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

LGTM modulo nit.

Copy link
Contributor

@frederick-vs-ja frederick-vs-ja left a comment

Choose a reason for hiding this comment

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

Ah. I was too rushed to approve this. You seemed to miss modifications of swap functions (both member and non-member ones) and make_optional.

We should verify

  • that optional<T&>::swap is always noexcept and only swap the internal pointers, and
  • that std::swap for optional<T&> behaves same as optional<T&>::swap, and
  • the constraints of the std::swap, and
  • modification to std::make_optional, especially that std::make_optional<X&>(x) can call different overloads until and since C++26.

@smallp-o-p
Copy link
Contributor Author

smallp-o-p commented Oct 27, 2025

  • modification to std::make_optional, especially that std::make_optional<X&>(x) can call different overloads until and since C++26.

Just to confirm, is the intended change to disallow std::make_optional for reference types? That is what I understood from the proposal.

@frederick-vs-ja
Copy link
Contributor

Just to confirm, is the intended change to disallow std::make_optional for reference types?

No. The intended change is to make std::make_optional<X&>(x) always return an optional<X&> in C++26. Before C++26, such well-formed std::make_optional<X&>(x) possibly returned an optional<decay_t<X>>.

E.g. given

const int n = 42;
auto o = std::make_optional<const int&>(n);

decltype(o) is optional<int> until C++26, but optional<const int&> since C++26.

Since C++26 we need to add a barrier template parameter to the first std::make_optional overload. We can follow the pattern for variant::visit (which generally disallows users to accidently specify the template argument).

struct __variant_visit_barrier_tag {
_LIBCPP_HIDE_FROM_ABI explicit __variant_visit_barrier_tag() = default;
};
template <__variant_visit_barrier_tag = __variant_visit_barrier_tag{}, class _Self, class _Visitor>
_LIBCPP_HIDE_FROM_ABI constexpr decltype(auto) visit(this _Self&& __self, _Visitor&& __visitor) {
return std::visit(std::forward<_Visitor>(__visitor), std::__forward_as<_Self, variant>(__self));
}

@smallp-o-p
Copy link
Contributor Author

smallp-o-p commented Oct 27, 2025

Just to confirm, is the intended change to disallow std::make_optional for reference types?

No. The intended change is to make std::make_optional<X&>(x) always return an optional<X&> in C++26. Before C++26, such well-formed std::make_optional<X&>(x) possibly returned an optional<decay_t<X>>.

E.g. given

const int n = 42;
auto o = std::make_optional<const int&>(n);

decltype(o) is optional<int> until C++26, but optional<const int&> since C++26.

Since C++26 we need to add a barrier template parameter to the first std::make_optional overload. We can follow the pattern for variant::visit (which generally disallows users to accidently specify the template argument).

struct __variant_visit_barrier_tag {
_LIBCPP_HIDE_FROM_ABI explicit __variant_visit_barrier_tag() = default;
};
template <__variant_visit_barrier_tag = __variant_visit_barrier_tag{}, class _Self, class _Visitor>
_LIBCPP_HIDE_FROM_ABI constexpr decltype(auto) visit(this _Self&& __self, _Visitor&& __visitor) {
return std::visit(std::forward<_Visitor>(__visitor), std::__forward_as<_Self, variant>(__self));
}

Does that mean the reference implementation beman::optional godbolt is incorrect? It explicitly disallows such a use of make_optional, but at the same time, the draft standard mentions no such restriction.

But at the same time, the proposal explicitly states that make_optional for the multi-argument overload should be ill-formed(?):

Adding a non-type template parameter as the first template parameter to the single argument make_optional
and mandating that the multi-argument version not request a reference type as the parameter, will diagnose
mistaken use of make_optional and preserve the existing behavior

I'm leaning towards your explanation but a clarification would be nice.

@frederick-vs-ja
Copy link
Contributor

Does that mean the reference implementation beman::optional godbolt is incorrect? It explicitly disallows such a use of make_optional, but at the same time, the draft standard mentions no such restriction.

I believe it's incorrect. There's nothing corresponding to that static_assert(!std::is_reference_v<T>, "..."); in the proposal or the current standard wording. If we want to make it correct, an LWG issue is necessary.

But at the same time, the proposal explicitly states that make_optional for the multi-argument overload should be ill-formed(?):

Adding a non-type template parameter as the first template parameter to the single argument make_optional
and mandating that the multi-argument version not request a reference type as the parameter, will diagnose
mistaken use of make_optional and preserve the existing behavior

If one provides more than one arguments to std::make_optional<T&>, the function call is automatically ill-formed due to the form of optional<T&>'s corresponding constructor. So we don't need to change the second or third std::make_optional overload.

@smallp-o-p
Copy link
Contributor Author

Reminder ping for more reviews 😄

Copy link
Contributor

@frederick-vs-ja frederick-vs-ja left a comment

Choose a reason for hiding this comment

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

I've just come up with some comments. Not sure whether any of them is blocking.

// static_assert(!has_iterator_aliases<std::optional<void (&)(int, char)>>);
static_assert(has_iterator_aliases<std::optional<int&>>);
static_assert(has_iterator_aliases<std::optional<const int&>>);
static_assert(!has_iterator_aliases<std::optional<int (&)[1]>>);
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like that we should achieve the following results, per the current resolution of LWG4308.

Suggested change
static_assert(!has_iterator_aliases<std::optional<int (&)[1]>>);
static_assert(has_iterator_aliases<std::optional<int (&)[1]>>);
static_assert(!has_iterator_aliases<std::optional<int (&)[]>>);

The product code needs to be correspondingly adjusted. We can do this in a following-up PR as LWG4308 is not formally accepted yet (but it will be soon).

assert(test<int&>());
assert(test<char&>());
assert(test<const int&>());
assert(test<const char&>());
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto, if you think it's better not to delay implementing LWG4308, cases for int (&)[42] and its friends should also be added.

Copy link
Contributor

@frederick-vs-ja frederick-vs-ja left a comment

Choose a reason for hiding this comment

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

Oh... I forgot the following partial specialization. Looks like that it isn't added yet.

template<class T>
constexpr bool ranges::enable_borrowed_range<optional<T&>> = true;

I think we should test that

  • ranges::enable_borrowed_range<optional<T&>> is true for all T, even if T is an array of unknown bound or a function type;
  • ranges::borrow_range<optional<T&>> is true if and only if ranges::range<optional<T&>> is true. It should be false when T is a function type.

@smallp-o-p
Copy link
Contributor Author

smallp-o-p commented Nov 8, 2025

Oh... I forgot the following partial specialization. Looks like that it isn't added yet.

template<class T>
constexpr bool ranges::enable_borrowed_range<optional<T&>> = true;

I think we should test that

* `ranges::enable_borrowed_range<optional<T&>>` is `true` for all `T`, even if `T` is an array of unknown bound or a function type;

* `ranges::borrow_range<optional<T&>>` is `true` if and only if `ranges::range<optional<T&>>` is `true`. It should be `false` when `T` is a function type.

I believe ranges::borrowed_range<optional<T&>> should also be false if T is an array type in our current implementation, at least until LWG4308 is accepted, which I don't think should be tackled in this PR.

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.

P2988R12: std::optional<T&>

6 participants