Skip to content

[libc++] Implement P3168R2: Give optional range support #149441

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ set(_defines
_LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR
_LIBCPP_ABI_BOUNDED_UNIQUE_PTR
_LIBCPP_ABI_BOUNDED_ITERATORS_IN_STD_ARRAY
_LIBCPP_ABI_BOUNDED_ITERATORS_IN_OPTIONAL
)
set(LIBCXX_ABI_DEFINES "${_defines}" CACHE STRING "")
2 changes: 1 addition & 1 deletion libcxx/docs/FeatureTestMacroTable.rst
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ Status
---------------------------------------------------------- -----------------
``__cpp_lib_not_fn`` ``202306L``
---------------------------------------------------------- -----------------
``__cpp_lib_optional_range_support`` *unimplemented*
``__cpp_lib_optional_range_support`` ``202406L``
---------------------------------------------------------- -----------------
``__cpp_lib_out_ptr`` ``202311L``
---------------------------------------------------------- -----------------
Expand Down
3 changes: 3 additions & 0 deletions libcxx/docs/ReleaseNotes/22.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ What's New in Libc++ 22.0.0?
==============================

Implemented Papers

- P3168R2: Give ``std::optional`` Range Support (`Github <https://github.com/llvm/llvm-project/issues/105430>`__)

------------------

- P2321R2: ``zip`` (`Github <https://github.com/llvm/llvm-project/issues/105169>`__) (The paper is partially implemented. ``zip_transform_view`` is implemented in this release)
Expand Down
2 changes: 1 addition & 1 deletion libcxx/docs/Status/Cxx2cPapers.csv
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"`P2747R2 <https://wg21.link/P2747R2>`__","``constexpr`` placement new","2024-06 (St. Louis)","|Complete|","20",""
"`P2997R1 <https://wg21.link/P2997R1>`__","Removing the common reference requirement from the indirectly invocable concepts","2024-06 (St. Louis)","|Complete|","19","Implemented as a DR against C++20. (MSVC STL and libstdc++ will do the same.)"
"`P2389R2 <https://wg21.link/P2389R2>`__","``dextents`` Index Type Parameter","2024-06 (St. Louis)","|Complete|","19",""
"`P3168R2 <https://wg21.link/P3168R2>`__","Give ``std::optional`` Range Support","2024-06 (St. Louis)","","",""
"`P3168R2 <https://wg21.link/P3168R2>`__","Give ``std::optional`` Range Support","2024-06 (St. Louis)","|Complete|","22",""
"`P3217R0 <https://wg21.link/P3217R0>`__","Adjoints to 'Enabling list-initialization for algorithms': find_last","2024-06 (St. Louis)","","",""
"`P2985R0 <https://wg21.link/P2985R0>`__","A type trait for detecting virtual base classes","2024-06 (St. Louis)","|Complete|","20",""
"`P0843R14 <https://wg21.link/P0843R14>`__","``inplace_vector``","2024-06 (St. Louis)","","",""
Expand Down
2 changes: 2 additions & 0 deletions libcxx/include/__iterator/wrap_iter.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +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 _Iter1>
Expand Down
68 changes: 68 additions & 0 deletions libcxx/include/optional
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ namespace std {
template <class T>
class optional;

template<class T>
constexpr bool ranges::enable_view<optional<T>> = true;
template<class T>
constexpr auto format_kind<optional<T>> = range_format::disabled;

template<class T>
concept is-derived-from-optional = requires(const T& t) { // exposition only
[]<class U>(const optional<U>&){ }(t);
Expand Down Expand Up @@ -102,6 +107,8 @@ namespace std {
class optional {
public:
using value_type = T;
using iterator = implementation-defined; // see [optional.iterators]
using const_iterator = implementation-defined; // see [optional.iterators]

// [optional.ctor], constructors
constexpr optional() noexcept;
Expand Down Expand Up @@ -135,6 +142,12 @@ namespace std {
// [optional.swap], swap
void swap(optional &) noexcept(see below ); // constexpr in C++20

// [optional.iterators], iterator support
constexpr iterator begin() noexcept;
constexpr const_iterator begin() const noexcept;
constexpr iterator end() noexcept;
constexpr const_iterator end() const noexcept;

// [optional.observe], observers
constexpr T const *operator->() const noexcept;
constexpr T *operator->() noexcept;
Expand Down Expand Up @@ -186,13 +199,18 @@ namespace std {
# include <__compare/three_way_comparable.h>
# include <__concepts/invocable.h>
# include <__config>
# include <__cstddef/ptrdiff_t.h>
# include <__exception/exception.h>
# include <__format/range_format.h>
# include <__functional/hash.h>
# include <__functional/invoke.h>
# include <__functional/unary_function.h>
# include <__fwd/functional.h>
# include <__iterator/bounded_iter.h>
# include <__iterator/wrap_iter.h>
# include <__memory/addressof.h>
# include <__memory/construct_at.h>
# include <__ranges/enable_view.h>
# include <__tuple/sfinae_helpers.h>
# include <__type_traits/add_pointer.h>
# include <__type_traits/conditional.h>
Expand Down Expand Up @@ -567,6 +585,14 @@ using __optional_sfinae_assign_base_t _LIBCPP_NODEBUG =
template <class _Tp>
class optional;

# if _LIBCPP_STD_VER >= 26
template <class _Tp>
constexpr bool ranges::enable_view<optional<_Tp>> = true;

template <class _Tp>
constexpr range_format format_kind<optional<_Tp>> = range_format::disabled;
# endif

# if _LIBCPP_STD_VER >= 20

template <class _Tp>
Expand All @@ -586,8 +612,22 @@ class _LIBCPP_DECLSPEC_EMPTY_BASES optional
private __optional_sfinae_assign_base_t<_Tp> {
using __base _LIBCPP_NODEBUG = __optional_move_assign_base<_Tp>;

# if _LIBCPP_STD_VER >= 26
using pointer = std::add_pointer_t<_Tp>;
using const_pointer = std::add_pointer_t<const _Tp>;
# endif

public:
using value_type = _Tp;
# if _LIBCPP_STD_VER >= 26
# ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_OPTIONAL
using iterator = __bounded_iter<__wrap_iter<pointer>>;
using const_iterator = __bounded_iter<__wrap_iter<const_pointer>>;
# else
using iterator = __wrap_iter<pointer>;
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if __wrap_iter is the right tool for the job here. __wrap_iter is basically __random_access_iterator except it has a weird name due to historical reasons. Recently, we've been trying to maintain relevant static information in the type of the iterators, see for example this discussion or this one yielding this issue.

As you have it right now, std::optional iterators would be interchangeable with std::vector iterators. I'm not certain that's what we want since that could lead to unintended misuse from users, and we're also dropping a lot of information on the floor (namely the fact that the range's size is at most 1).

My current thinking is that we should either:

  • Define an iterator that is specific to std::optional, or
  • Define an iterator type that encodes the static information about the maximum size of the range being N. Something like __statically_bounded_iterator<Underlying, N>, although that name clashes with libcxx/include/__iterator/static_bounded_iter.h which is used for hardening. The idea is similar, except the iterator used by optional doesn't need to physically maintain its bounds, it just wants to encode that static knowledge somewhere.

This actually makes me realize that there's something else we should probably do as well: we should probably provide an option to use hardened __static_bounded_iters in optional (behind an ABI macro).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I personally think the second option sounds good, but should go in a different PR of course. It might also be useful for other containers like inplace_vector.

As for the __static_bounded_iter suggestion, I originally implemented hardening with __bounded_iter (but kept it out of the PR to get some input first), which felt like the better choice since we can't know if the optional is going to have a range of 0 or 1 at compile time(?)

Copy link
Contributor

Choose a reason for hiding this comment

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

@ldionne What improvement do you see by providing an optional-specific iterator? I don't really see how such an iterator would buy us anything. It would be an improvement for bounds-safety, but that's not provided here anyways.

Copy link
Contributor

@frederick-vs-ja frederick-vs-ja Jul 25, 2025

Choose a reason for hiding this comment

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

Considering optional<T(&)[]> (note the missing bound) and optional<T(&)(Args)>, I wonder whether they should have iterator. Certainly, such an iterator can't be a contiguous iterator (and not even a real iterator in the case of optional<T(&)(Args)>), and our wrappers don't seem to work with them. If we decide they should have iterator, we probably need to introduce optional-specific iterators. @philnik777 @ldionne

Copy link
Member

Choose a reason for hiding this comment

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

@philnik777 The improvement I see by having an optional-specific iterator is that we can't mistake it for a std::vector::iterator, for example. There might also be optimization opportunities by knowing that an iterator points to at most one element (basically that encodes the knowledge of a range having at most N elements where N is a compile-time constant). But I'm primarily interested in making the following fail:

std::vector<int> v = {...};
std::optional<int> opt = ...;

std::sort(v.begin(), opt.end()); // wtf?

If we use __wrap_iter, that code would compile despite being obviously wrong. A bunch of other stuff would also work, such as v.begin() != opt.end(), and none of it makes sense.

Copy link
Member

Choose a reason for hiding this comment

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

@frederick-vs-ja I fail to see what's wrong with the optional types you mentioned. Is it because you can't have a reference to a function as an iterator's value_type?

Copy link
Member

Choose a reason for hiding this comment

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

@smallp-o-p

which felt like the better choice since we can't know if the optional is going to have a range of 0 or 1 at compile time(?)

Yes, you're right, __static_bounded_iter doesn't work for that reason. I think your current usage of __bounded_iter is the best we can do for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it because you can't have a reference to a function as an iterator's value_type?

Yes. And for T(&)[], operator++ doesn't work (Godbolt link).

[optional.ref.iterators]/1 requires some weird operator<T&>::iterator to be contiguous_iterator, which is defective.

I'm not sure whether we want these iterator types and corresponding begin/end. Perhaps someone would want to write

for (auto& fun : opt_func_ref) {
   fun(args);
}

But I'm not sure whether we want to support or disallow this.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, that's a good point. That's LWG issue territory then, isn't it? What would you think of not providing the iterator typedef in the cases that we know the resulting typedef is not going to be a proper iterator? That would close this door for the time being, until WG21 decides what to do with it design-wise.

If you agree @frederick-vs-ja, then @smallp-o-p this patch should basically change to something like this:

template <class _Tp, class = void>
struct __optional_iterator_aliases { };

template <class _Tp>
struct __optional_iterator_aliases<_Tp, __enable_if_t<!is_function<__libcpp_remove_reference_t<_Tp> >::value &&
                                                      !is_unbounded_array<__libcpp_remove_reference_t<_Tp> >::value> > {
private:
  // note: I am purposefully using private names here because I believe we want
  // to provide ::pointer and ::const_pointer unconditionally.
  using __pointer = std::add_pointer_t<_Tp>;
  using __const_pointer = std::add_pointer_t<const _Tp>;

public:
# if _LIBCPP_STD_VER >= 26
#   ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_OPTIONAL
  using iterator       = __bounded_iter<__wrap_iter<__pointer>>;
  using const_iterator = __bounded_iter<__wrap_iter<__const_pointer>>;
 #      else
   using iterator       = __wrap_iter<__pointer>;
   using const_iterator = __wrap_iter<__const_pointer>;
#   endif
# 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_aliases<_Tp> {
  // ...

We would also want to add a negative test like this one:

template <class T>
concept has_iterator_aliases = requires {
  typename T::iterator;
  typename T::const_iterator;
};

static_assert(!has_iterator_aliases<std::optional<int (&)[]>>);
static_assert(!has_iterator_aliases<std::optional<void (&)(int, char)>>);

That test would need to live under libcxx/test/libcxx/utilities/optional/optional.iterator/iterator.compile.pass.cpp. Notice how this is under libcxx/test/libcxx instead of libcxx/test/std: that is because this test would be specific to libc++ (until WG21 decides what's the Standard behavior). We put tests that check libc++ specific implementation details under a separate directory so that other standard libraries can reuse the libcxx/test/std directory to check their own conformance status.

Finally, we'd also need to make sure that we have positive testing coverage for std::optional<int (&)[]>::pointer and std::optional<void (&)(int, char)>::pointer under libcxx/test/std. That actually makes me realize that this patch is missing such coverage even for std::optional<int> (unless I missed it) -- so it should be added. We generally test such things in a test named some/path/types.pass.cpp, and it looks like we already have one for std::optional here: libcxx/test/std/utilities/optional/optional.object/types.pass.cpp. You will want to augment it with additional tests guarded under TEST_STD_VER >= 26, a macro that can be found in test_macros.h.

Sorry if you already know some of that, I'm trying to give you as much context as possible since I know this is amongst your first patches.

using const_iterator = __wrap_iter<const_pointer>;
# endif
# endif

using __trivially_relocatable _LIBCPP_NODEBUG =
conditional_t<__libcpp_is_trivially_relocatable<_Tp>::value, optional, void>;
Expand Down Expand Up @@ -792,6 +832,34 @@ 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());
Expand Down
2 changes: 1 addition & 1 deletion libcxx/include/version
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ __cpp_lib_void_t 201411L <type_traits>
# define __cpp_lib_mdspan 202406L
# undef __cpp_lib_not_fn
# define __cpp_lib_not_fn 202306L
// # define __cpp_lib_optional_range_support 202406L
# define __cpp_lib_optional_range_support 202406L
# undef __cpp_lib_out_ptr
# define __cpp_lib_out_ptr 202311L
// # define __cpp_lib_philox_engine 202406L
Expand Down
11 changes: 10 additions & 1 deletion libcxx/modules/std/optional.inc
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@
export namespace std {
// [optional.optional], class template optional
using std::optional;

#if _LIBCPP_STD_VER >= 26
// [optional.iterators], iterator support
namespace ranges {
using std::ranges::enable_view;
}
#endif
// [optional.nullopt], no-value state indicator
using std::nullopt;
using std::nullopt_t;

// [optional.bad.access], class bad_optional_access
using std::bad_optional_access;

#if _LIBCPP_STD_VER >= 26
using std::format_kind;
#endif

// [optional.relops], relational operators
using std::operator==;
using std::operator!=;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,17 +152,11 @@
# error "__cpp_lib_optional should have the value 202110L in c++26"
# endif

# if !defined(_LIBCPP_VERSION)
# ifndef __cpp_lib_optional_range_support
# error "__cpp_lib_optional_range_support should be defined in c++26"
# endif
# if __cpp_lib_optional_range_support != 202406L
# error "__cpp_lib_optional_range_support should have the value 202406L in c++26"
# endif
# else
# ifdef __cpp_lib_optional_range_support
# error "__cpp_lib_optional_range_support should not be defined because it is unimplemented in libc++!"
# endif
# ifndef __cpp_lib_optional_range_support
# error "__cpp_lib_optional_range_support should be defined in c++26"
# endif
# if __cpp_lib_optional_range_support != 202406L
# error "__cpp_lib_optional_range_support should have the value 202406L in c++26"
# endif

#endif // TEST_STD_VER > 23
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7443,17 +7443,11 @@
# error "__cpp_lib_optional should have the value 202110L in c++26"
# endif

# if !defined(_LIBCPP_VERSION)
# ifndef __cpp_lib_optional_range_support
# error "__cpp_lib_optional_range_support should be defined in c++26"
# endif
# if __cpp_lib_optional_range_support != 202406L
# error "__cpp_lib_optional_range_support should have the value 202406L in c++26"
# endif
# else
# ifdef __cpp_lib_optional_range_support
# error "__cpp_lib_optional_range_support should not be defined because it is unimplemented in libc++!"
# endif
# ifndef __cpp_lib_optional_range_support
# error "__cpp_lib_optional_range_support should be defined in c++26"
# endif
# if __cpp_lib_optional_range_support != 202406L
# error "__cpp_lib_optional_range_support should have the value 202406L in c++26"
# endif

# ifndef __cpp_lib_out_ptr
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//===----------------------------------------------------------------------===//
//
// 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>

// constexpr iterator optional::begin() noexcept;
// constexpr const_iterator optional::begin() noexcept;

#include <cassert>
#include <iterator>
#include <optional>
#include <type_traits>
#include <utility>

template <typename T>
constexpr bool test() {
const std::optional<T> opt{T{}};
std::optional<T> nonconst_opt{T{}};

{ // begin() is marked noexcept
static_assert(noexcept(opt.begin()));
static_assert(noexcept(nonconst_opt.begin()));
}

{ // Dereferencing an iterator at the beginning == indexing the 0th element, and that calling begin() again return the same iterator.
auto iter1 = opt.begin();
auto iter2 = nonconst_opt.begin();
Comment on lines +33 to +34
Copy link
Member

Choose a reason for hiding this comment

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

Both of these optionals are non-const. I believe that using std::as_const consistently throughout your tests would resolve the possibility for these kinds of mixups!

assert(*iter1 == iter1[0]);
assert(*iter2 == iter2[0]);
assert(iter1 == opt.begin());
assert(iter2 == nonconst_opt.begin());
}

{ // Calling begin() multiple times on a disengaged optional returns the same iterator.
std::optional<T> disengaged{std::nullopt};
auto iter1 = disengaged.begin();
auto iter2 = std::as_const(disengaged).begin();
assert(iter1 == disengaged.begin());
assert(iter2 == std::as_const(disengaged).begin());
}

return true;
}

constexpr bool tests() {
assert(test<int>());
assert(test<char>());
assert(test<const int>());
assert(test<const char>());
return true;
}

int main(int, char**) {
assert(tests());
static_assert(tests());

return 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//===----------------------------------------------------------------------===//
//
// 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>

// constexpr iterator optional::end() noexcept;
// constexpr const_iterator optional::end() noexcept;

#include <cassert>
#include <iterator>
#include <ranges>
#include <optional>

template <typename T>
constexpr bool test() {
std::optional<T> disengaged{std::nullopt};
const std::optional<T> disengaged2{std::nullopt};

{ // end() is marked noexcept
static_assert(noexcept(disengaged.end()));
static_assert(noexcept(disengaged2.end()));
}

{ // end() == begin() and end() == end() if the optional is disengaged
auto it = disengaged.end();
auto it2 = disengaged2.end();

assert(it == disengaged.begin());
assert(disengaged.begin() == it);
assert(it == disengaged.end());

assert(it2 == disengaged2.begin());
assert(disengaged2.begin() == it2);
assert(it2 == disengaged2.end());
}

std::optional<T> engaged{T{}};
const std::optional<T> engaged2{T{}};

{ // end() != begin() if the optional is engaged
auto it = engaged.end();
auto it2 = engaged2.end();

assert(it != engaged.begin());
assert(engaged.begin() != it);

assert(it2 != engaged2.begin());
assert(engaged2.begin() != it2);
}

return true;
}

constexpr bool tests() {
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.

I think I like how you organized the tests now. FYI only - I am not asking for any changes: there is an alternative to iterate over multiple types a helper types::for_each(types::integer_types(), IntegerTypesTest{}); an example usage is here: https://github.com/llvm/llvm-project/blob/9ae7490c6251ec60ab978bb818e89ca586e0c9c9/libcxx/test/std/ranges/range.factories/range.iota.view/indices.pass.cpp


return true;
}

int main(int, char**) {
assert(tests());
static_assert(tests());

return 0;
}
Loading
Loading