Document #: | P3697R0 |
Date: | 2025-05-15 |
Project: | Programming Language C++ |
Audience: |
Library Evolution, Library |
Reply-to: |
Konstantin Varlamov <varconst@apple.com> Louis Dionne <ldionne@apple.com> Alisdair Meredith <ameredith1@bloomberg.net> |
This paper is a small follow-up to P3471R4 “Standard Library Hardening” that hardens the preconditions of a few more library functions. All these functions satisfy the same criteria as the ones in the original paper:
In addition (unlike the “Notable omissions” section in the original paper), these functions are comparatively standalone and don’t involve complex interactions with other members of their respective classes. The additions here are essentially oversights in the original paper that were noticed during offline discussions with various people.
We propose to harden the relevant preconditions of the functions listed below.
basic_stacktrace
current(skip, max_depth)
.If skip + max_depth
overflows, an implementation will attempt iterating over a range [i, j]
where
j < i
,
quickly going out of bounds;
operator[]
.This prevents a potential out-of-bounds access and brings the Library
closer to an ideal state where an operator[]
can never cause a memory safety issue in a hardened implementation.
shared_ptr<T[N]>
operator[]
.In the case where the shared_ptr
points to an array with a statically known size, we are able to perform
bounds checking.
view_interface
front
;back
.While we would like to explore hardening Ranges in a dedicated paper,
it has been observed that view_interface::front
and view_interface::back
are the only front
and
back
functions in the Library that
aren’t hardened, which might come as an unpleasant surprise to
users.
counted_iterator
operator*
;operator[]
.Unlike most iterator types,
counted_iterator
contains
some of the data necessary to check whether it is valid upon
dereference. Specifically, it can check for going out of bounds past
the end (since it needs to store the remaining
length
of the sequence and can use
0
as an
indicator of reaching the end), but not before the beginning
(the iterator can be rewound but it does not store the original
length
and thus cannot check whether
it’s being rewound past the original beginning). To visualize this,
counted_iterator
can check the right
bound but not the left bound. We believe that hardening the
preconditions that already exist and can be easily checked is valuable
from a pragmative perspective and preferable to not hardening the class
at all.
Note that we are not hardening operator->
.
Unlike with other dereference operators, no immediate access happens
within the Library. In the current specification, it is not possible to
create an invalid (past the bounds, other than the special
“one-past-end”) counted_iterator
without invoking undefined behavior. Existing preconditions check
against the iterator being incremented beyond the end; there
are no explicit preconditions for decrementing the iterator,
but an implicit precondition follows from the fact that decrementing the
iterator to point before the beginning would result in forming an
invalid pointer, which is UB in itself. As mentioned above, for
hardening purposes we can only easily check against going past the end
(only half of the invariant).
counted_iterator(i, n)
;operator++
;operator+=
;operator-=
.These functions don’t access the underlying iterator but might end up
making length
negative, going past
the end of the sequence. Do they need to be hardened, given that we also
harden the dereference operators? Hardening seems preferable; first,
from a more theoretical perspective, maintaining (to the extent
possible) the invariant that the iterator is always valid makes it
easier to reason about the class; also, setting
current
to an invalid pointer is
undefined behavior in itself. Second, ensuring the iterator is always
valid (or one-past-end) also protects the operator->
for which no explicit precondition currently exists. As stated above,
counted_iterator
only contains data
necessary for checking against going past the end but not before the
beginning (thus operator--
is not being hardened).
iter_move(counted_iterator)
;iter_swap(counted_iterator, counted_iterator)
.While iter_move
and
iter_swap
don’t access memory
directly, if the iterator (or one of the iterators) is past the end, it
creates a situation where an out-of-bounds access is almost guaranteed
and can no longer be easily checked.
common_iterator
common_iterator(const common_iterator<I2, S2>&)
;operator=
;operator==(const common_iterator&, const common_iterator<I2, S2>&)
;operator-(const common_iterator&, const common_iterator<I2, S2>&)
.common_iterator
is specified in
terms of a variant
, and some of its
functions have a precondition that the
variant
is not in the invalid state
(not valueless_by_exception
). Thus,
unlike variant
member functions that
are required to handle the case of
valueless_by_exception
gracefully
(by throwing or returning a sentinel value), it is undefined behavior to
access a valueless common_iterator
–
in practice, an implementation will likely access uninitialized memory.
This applies to any modifications to the underlying iterator object, not
only dereferences.
operator*
;operator->
;operator++
;operator++(int)
;iter_move(const common_iterator&)
;iter_swap(const common_iterator&, const common_iterator<I2, S2>&)
.Similarly, a few common_iterator
functions have a precondition that the
common_iterator
holds an iterator
and not a sentinel, violating which might also result in a memory safety
issue.
shared_ptr
shared_ptr<T[N]>::operator[]
Modify 20.3.2.2.6 [util.smartptr.shared.obs] around paragraph 8 as indicated:
element_type& operator[](ptrdiff_t i) const;
8 Preconditions:
get() != nullptr
.&& i >= 0
. IfT
isU[N]
,i < N
9 Hardened preconditions:
i >= 0
. IfT
isU[N]
,i < N
.10 Returns:
get()[i]
.11 Throws: Nothing.
view_interface
view_interface::front
Modify 25.5.3.2 [view.interface.members] paragraph 1 as indicated:
constexpr decltype(auto) front() requires forward_range<D>; constexpr decltype(auto) front() const requires forward_range<const D>;
1 Hardened
Ppreconditions:!empty()
istrue
.2 Effects: Equivalent to:
return *ranges::begin(derived());
view_interface::back
Modify 25.5.3.2 [view.interface.members] paragraph 3 as indicated:
constexpr decltype(auto) back() requires bidirectional_range<D> && common_range<D>; constexpr decltype(auto) back() const requires bidirectional_range<const D> && common_range<const D>;
3 Hardened
Ppreconditions:!empty()
istrue
.4 Effects: Equivalent to:
return *ranges::prev(ranges::end(derived()));
counted_iterator
counted_iterator::operator*
Modify 24.5.7.4 [counted.iter.elem] paragraph 1 as indicated:
constexpr decltype(auto) operator*(); constexpr decltype(auto) operator*() const requires dereferenceable<const I>;
1 Hardened
Ppreconditions:length > 0
istrue
.2 Effects: Equivalent to:
return *current;
counted_iterator::operator[]
Modify 24.5.7.4 [counted.iter.elem] paragraph 4 as indicated:
constexpr decltype(auto) operator[](iter_difference_t<I> n) const requires random_access_iterator<I>;
4 Hardened
Ppreconditions:n < length
istrue
.5 Effects: Equivalent to:
return current[n];
iter_move(const counted_iterator&)
Modify 24.5.7.7 [counted.iter.cust] paragraph 1 as indicated:
friend constexpr decltype(auto) iter_move(const counted_iterator& i) noexcept(noexcept(ranges::iter_move(i.current))) requires input_iterator<I>;
1 Hardened
Ppreconditions:i.length > 0
istrue
.2 Effects: Equivalent to:
return ranges::iter_move(i.current);
iter_swap(const counted_iterator&, const counted_iterator<I2>&)
Modify 24.5.7.7 [counted.iter.cust] paragraph 3 as indicated:
template<indirectly_swappable<I> I2> friend constexpr void iter_swap(const counted_iterator& x, const counted_iterator<I2>& y) noexcept(noexcept(ranges::iter_swap(x.current, y.current)));
3 Hardened
Ppreconditions: Bothx.length > 0
andy.length > 0
aretrue
.4 Effects: Equivalent to
ranges::iter_swap(x.current, y.current)
.
counted_iterator::counted_iterator(I, iter_difference_t<T>)
Modify 24.5.7.2 [counted.iter.const] paragraph 1 as indicated:
constexpr counted_iterator(I i, iter_difference_t<I> n);
1 Hardened
Ppreconditions:n >= 0
.2 Effects: Initializes
current
withstd::move(i)
andlength
withn
.
counted_iterator::operator++
Modify 24.5.7.5 [counted.iter.nav] paragraph 1 as indicated:
constexpr counted_iterator& operator++();
1 Hardened
Ppreconditions:length > 0
istrue
.2 Effects: Equivalent to:
++current; --length; return *this;
counted_iterator::operator++(int)
Modify 24.5.7.5 [counted.iter.nav] paragraph 3 as indicated:
constexpr decltype(auto) operator++(int);
3 Hardened
Ppreconditions:length > 0
istrue
.4 Effects: Equivalent to:
--length; try { return current++; } catch(...) { ++length; throw; }
counted_iterator::operator+=
Modify 24.5.7.5 [counted.iter.nav] paragraph 10 as indicated:
constexpr counted_iterator& operator+=(iter_difference_t<I> n) requires random_access_iterator<I>;
10 Hardened
Ppreconditions:n <= length
istrue
.11 Effects: Equivalent to:
current += n; length -= n; return *this;
counted_iterator::operator-=
Modify 24.5.7.5 [counted.iter.nav] paragraph 17 as indicated:
constexpr counted_iterator& operator-=(iter_difference_t<I> n) requires random_access_iterator<I>;
17 Hardened
Ppreconditions:-n <= length
istrue
.18 Effects: Equivalent to:
current -= n; length += n; return *this;
common_iterator
common_iterator(const common_iterator<I2, S2>&)
Modify 24.5.5.3 [common.iter.const] paragraph 3 as indicated:
template<class I2, class S2> requires convertible_to<const I2&, I> && convertible_to<const S2&, S> constexpr common_iterator(const common_iterator<I2, S2>& x);
3 Hardened
Ppreconditions:x.v_.valueless_by_exception()
isfalse
.4 Effects: Initializes
v_
as if byv_{in_place_index<i>, get<i>(x.v_)}
, wherei
isx.v_.index()
.
common_iterator::operator=
Modify 24.5.5.3 [common.iter.const] paragraph 5 as indicated:
template<class I2, class S2> requires convertible_to<const I2&, I> && convertible_to<const S2&, S> && assignable_from<I&, const I2&> && assignable_from<S&, const S2&> constexpr common_iterator& operator=(const common_iterator<I2, S2>& x);
5 Hardened
Ppreconditions:x.v_.valueless_by_exception()
isfalse
.6 Effects: Equivalent to:
(6.1) If
v_.index() == x.v_.index()
, thenget<i>(v_) = get<i>(x.v_)
.(6.2) Otherwise,
v_.emplace<i>(get<i>(x.v_))
.where
i
isx.v_.index()
.
common_iterator::operator*
Modify 24.5.5.4 [common.iter.access] paragraph 1 as indicated:
constexpr decltype(auto) operator*(); constexpr decltype(auto) operator*() const requires dereferenceable<const I>;
1 Hardened
Ppreconditions:holds_alternative<I>(v_)
istrue
.
common_iterator::operator->
Modify 24.5.5.4 [common.iter.access] paragraph 4 as indicated:
constexpr auto operator->() const requires see below;
3 The expression in the requires-clause is equivalent to:
indirectly_readable<const I> && (requires(const I& i) { i.operator->(); } || is_reference_v<iter_reference_t<I>> || constructible_from<iter_value_t<I>, iter_reference_t<I>>)
4 Hardened
Ppreconditions:holds_alternative<I>(v_)
istrue
.
common_iterator::operator++
Modify 24.5.5.5 [common.iter.nav] paragraph 1 as indicated:
constexpr common_iterator& operator++();
1 Hardened
Ppreconditions:holds_alternative<I>(v_)
istrue
.2 Effects: Equivalent to
++get<I>(v_)
.3 Returns:
*this
.
common_iterator::operator++(int)
Modify 24.5.5.5 [common.iter.nav] paragraph 4 as indicated:
constexpr decltype(auto) operator++(int);
4 Hardened
Ppreconditions:holds_alternative<I>(v_)
istrue
.5 Effects: If
I
modelsforward_iterator
, equivalent to:common_iterator tmp = *this; ++*this; return tmp;
operator==(const common_iterator&, const common_iterator<I2, S2>&)
Modify 24.5.5.6 [common.iter.cmp] paragraphs 1 and 3 as indicated:
template<class I2, sentinel_for<I> S2> requires sentinel_for<S, I2> friend constexpr bool operator==( const common_iterator& x, const common_iterator<I2, S2>& y);
1 Hardened
Ppreconditions:x.v_.valueless_by_exception()
andy.v_.valueless_by_exception()
are eachfalse
.2 Returns:
true
ifi == j
, and otherwiseget<i>(x.v_) == get<j>(y.v_)
, wherei
isx.v_.index()
andj
isy.v_.index()
.template<class I2, sentinel_for<I> S2> requires sentinel_for<S, I2> && equality_comparable_with<I, I2> friend constexpr bool operator==( const common_iterator& x, const common_iterator<I2, S2>& y);
3 Hardened
Ppreconditions:x.v_.valueless_by_exception()
andy.v_.valueless_by_exception()
are eachfalse
.4 Returns:
true
ifi
andj
are each1
, and otherwiseget<i>(x.v_) == get<j>(y.v_)
, wherei
isx.v_.index()
andj
isy.v_.index()
.
operator-(const common_iterator&, const common_iterator<I2, S2>&)
Modify 24.5.5.6 [common.iter.cmp] paragraph 5 as indicated:
template<sized_sentinel_for<I> I2, sized_sentinel_for<I> S2> requires sized_sentinel_for<S, I2> friend constexpr iter_difference_t<I2> operator-( const common_iterator& x, const common_iterator<I2, S2>& y);
5 Hardened
Ppreconditions:x.v_.valueless_by_exception()
andy.v_.valueless_by_exception()
are eachfalse
.6 Returns:
0
ifi
andj
are each1
, and otherwiseget<i>(x.v_) - get<j>(y.v_)
, wherei
isx.v_.index()
andj
is y.v_.index()
.
iter_move(const common_iterator&)
Modify 24.5.5.7 [common.iter.cust] paragraph 1 as indicated:
friend constexpr decltype(auto) iter_move(const common_iterator& i) noexcept(noexcept(ranges::iter_move(declval<const I&>()))) requires input_iterator<I>;
1 Hardened
Ppreconditions:holds_alternative<I>(i.v_)
istrue
.2 Effects: Equivalent to:
return ranges::iter_move(get<I>(i.v_));
iter_swap(const common_iterator&, const common_iterator<I2, S2>&)
Modify 24.5.5.7 [common.iter.cust] paragraph 3 as indicated:
template<indirectly_swappable<I> I2, class S2> friend constexpr void iter_swap(const common_iterator& x, const common_iterator<I2, S2>& y) noexcept(noexcept(ranges::iter_swap(declval<const I&>(), declval<const I2&>())));
3 Hardened
Ppreconditions:holds_alternative<I>(x.v_)
andholds_alternative<I2>(y.v_)
are eachtrue
.4 Effects: Equivalent to
ranges::iter_swap(get<I>(x.v_)
,get<I2>(y.v_))
.
basic_stacktrace
basic_stacktrace::current
Modify 19.6.4.2 [stacktrace.basic.cons] paragraph 5 as indicated:
static basic_stacktrace current(size_type skip, size_type max_depth, const allocator_type& alloc = allocator_type()) noexcept;
4 Let
t
be a stacktrace as-if obtained viabasic_stacktrace::current(alloc)
. Letn
bet.size()
.5 Hardened
Ppreconditions:skip <= skip + max_depth
istrue
.6 Returns: A
basic_stacktrace
object whereframes_
is direct-non-list-initialized from argumentst.begin() + min(n, skip), t.begin() + min(n, skip + max_depth)
, andalloc
, or an emptybasic_stacktrace
object if the initialization offrames_
failed.
basic_stacktrace::operator[]
Modify 19.6.4.3 [stacktrace.basic.obs] paragraph 10 as indicated:
const_reference operator[](size_type frame_no) const;
10 Hardened
Ppreconditions:frame_no < size()
istrue
.11 Returns:
frames_[frame_no]
.12 Throws: Nothing.
Modify 17.3.2 [version.syn] paragraph 3 as indicated:
3 Additionally, each of the following macros are defined in a hardened implementation:
#define __cpp_lib_hardened_array 202502L // also in <array>+ #define __cpp_lib_hardened_basic_stacktrace 202???L // also in <stacktrace>
#define __cpp_lib_hardened_basic_string 202502L // also in <string>
#define __cpp_lib_hardened_basic_string_view 202502L // also in <string_view>
#define __cpp_lib_hardened_bitset 202502L // also in <bitset>+ #define __cpp_lib_hardened_common_iterator 202???L // also in <iterator>
+ #define __cpp_lib_hardened_counted_iterator 202???L // also in <iterator>
#define __cpp_lib_hardened_deque 202502L // also in <deque>
#define __cpp_lib_hardened_expected 202502L // also in <expected>
#define __cpp_lib_hardened_forward_list 202502L // also in <forward_list>
#define __cpp_lib_hardened_inplace_vector 202502L // also in <inplace_vector>
#define __cpp_lib_hardened_list 202502L // also in <list>
#define __cpp_lib_hardened_mdspan 202502L // also in <mdspan>
#define __cpp_lib_hardened_optional 202502L // also in <optional>+ #define __cpp_lib_hardened_shared_ptr_array 202???L // also in <memory>
#define __cpp_lib_hardened_span 202502L // also in <span>
#define __cpp_lib_hardened_valarray 202502L // also in <valarray>
#define __cpp_lib_hardened_vector 202502L // also in <vector>+ #define __cpp_lib_hardened_view_interface 202???L // also in <ranges>