Skip to content

Add unchecked_slice - #10

Merged
tcbrindle merged 19 commits into
mainfrom
pr/unchecked_slice
Sep 10, 2026
Merged

Add unchecked_slice#10
tcbrindle merged 19 commits into
mainfrom
pr/unchecked_slice

Conversation

@tcbrindle

Copy link
Copy Markdown
Owner

As the name suggests, this is just like slice<T> except that it does no bounds checking -- so most of its operations cause UB if their preconditions are not met. Its iterators are just raw pointers. There is no unchecked_slice::at(), because it's not clear what semantics it should have. Like slice, unchecked_slice is not publicly copyable or movable.

With this in place, we can give slice<T> a public member named unchecked of type unchecked_slice<T>, and implement the (bounds checking) slice methods on top of that. Array pointers (tcb::pointer<T[]>) still deref to slice<T>& or slice<T> const& as before.

The idea is to make bounds checked operations the default, but allow users to explicitly opt in to using unchecked operations when necessary, in a way that is clear and obvious in source code. For example:

auto& slice = *ptr;

int i = slice[10]; // bounds checked
int j = slice.unchecked[10]; // explicitly not bounds checked

my_algorithm(slice.begin(), slice.end()); // bounds checked iters
my_algorithm(slice.unchecked.begin(), slice.unchecked.end()); // explicitly unchecked iters

my_range_algorithm(slice); // uses checked iters
my_range_algorithm(slice.unchecked); // uses unchecked iters

`std::ranges::equal_to` uses the "implementation-defined strict weak ordering over pointers", but returns a boolean directly rather than needing to do `compare_three_way(a, b) == 0`
As the name suggests, this is just like `slice<T>` except that it does no bounds checking -- so most of its operations cause UB if their preconditions are not met. Its iterators are just raw pointers. There is no `unchecked_slice::at()`, because it's not clear what semantics it should have. Like `slice`, `unchecked_slice` is not publicly copyable or movable.
This gives `slice<T>` a public member variable of type `unchecked_slice<T>`.

The idea is that bounds checking is the default, but users can explicitly opt out where necessary, in a way that is clear and obvious in source code. For example:

    auto& slice = *ptr;

    int i = slice[10]; // bounds checked
    int j = slice.unchecked[10]; // explicitly not bounds checked

    my_algo(slice.begin(), slice.end()); // bounds checked iters
    my_algo(slice.unchecked.begin(),
                   slice.unchecked.end()); // explicitly unchecked

    my_range_algo(slice); // uses checked iters
    my_range_algo(slice.unchecked); // uses unchecked iters
This removes the option to define TCB_PTR_USE_UNCHECKED_ITERATORS, as users can now say `slice.unchecked.begin()` etc.
For many types, `operator==` is cheaper than `operator<` so try that first if the element type does not support spaceship directly.
...and make the "position" constructor private
Comment thread include/tcb/pointer.hpp Fixed
Comment thread include/tcb/pointer.hpp Fixed
Comment thread include/tcb/pointer.hpp Fixed
Comment thread include/tcb/pointer.hpp Fixed
Comment thread include/tcb/pointer.hpp Fixed
Comment thread include/tcb/pointer.hpp Fixed
Comment thread include/tcb/pointer.hpp Fixed
Comment thread include/tcb/pointer.hpp Fixed
The original goal of checked_iterator<T> was to prevent any attempt to form an out-of-bounds iterator. This is certainly safe, but ends up doing more checks than we actually need to prevent UB.

With this change, we instead perform bounds checks only when dereferencing (or attempting to form a pointer-to-element). This means that it's now possible to form an out-of-bounds iterator, so long as you never actually try to access the element it would be pointing at.
Hopefully this will keep MSVC happy
The object pointer and void pointer specialisations of `tcb::pointer` do not permit a null state -- this is an important design goal for safety, and what allows us to re-use the null representation for our `optional` specialisation.

The intention was that this would apply to the array specialisation as well -- so it had no default constructor, and attempting to pass `nullptr` to `from_address_with_size()` was a runtime error.

Unfortunately however, I didn't consider the case of forming an array pointer to a default-constructed vector, which usually has a null data pointer. This means that it's already possible to legitimately get a null `ptr<T[]>`, which in turn means that `optional<ptr<T[]>>` can't tell the difference between an empty optional and one initialized from a default-constructed vector. That's bad.

This means that unfortunately we can no longer use our optional specialisation for array pointers, and they will instead use the primary optional template. We also may as well explicitly acknowledge the null state by adding a default constructor and allowing a (suitably typed) null pointer to be passed to `from_address_with_size()`, provided the given size is zero.

This is a bit of a shame, but it's worth noting that this is not a safety problem -- unlike with object pointers, a default-constructed array pointer will deref to a valid, useable (if zero-sized) `slice`. We still maintain the invariant that if `data()` is null then `size()` must be zero, which is the important bit from a safety point of view.
The single-object and void specialisations have the invariant that they're never null, so converting to bool would always return true.

For array pointers, a pointer to a null array is still safe to dereference, so it's not clear that the value of the bool conversion operator is -- users can always test `data() == nullptr` themselves if they need want to.
...or more specifically, when the standard library advertises optional range support, because it's a bit weird for a specialisation to add a feature that the primary template doesn't have.

Defining `TCB_PTR_OPTIONAL_RANGE_SUPPORT` adds `begin()` and `end()` to our optional specialisation in all language modes.
"the program is ill-formed" == static_assert
"does not participate in overload resolution" = requires clause
Comment thread tests/pointer.test.cpp Dismissed
Comment thread tests/pointer.test.cpp Dismissed
...or more precisely, when the standard library advertises support for them. Again, this can be overridden by defining `TCB_PTR_OPTIONAL_MONADIC_SUPPORT`, in which case the operations will be provided in all language modes
@tcbrindle
tcbrindle merged commit 070e561 into main Sep 10, 2026
40 checks passed
@tcbrindle
tcbrindle deleted the pr/unchecked_slice branch September 11, 2026 14:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants