Add unchecked_slice - #10
Merged
Merged
Conversation
`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
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.
tcbrindle
force-pushed
the
pr/unchecked_slice
branch
from
September 9, 2026 16:54
7c87a15 to
3939491
Compare
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
tcbrindle
force-pushed
the
pr/unchecked_slice
branch
from
September 10, 2026 16:20
e9520b4 to
6c8ad13
Compare
...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
force-pushed
the
pr/unchecked_slice
branch
from
September 10, 2026 16:31
6c8ad13 to
d1a3572
Compare
...and also add a test for value_or()
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 nounchecked_slice::at(), because it's not clear what semantics it should have. Likeslice,unchecked_sliceis not publicly copyable or movable.With this in place, we can give
slice<T>a public member nameduncheckedof typeunchecked_slice<T>, and implement the (bounds checking)slicemethods on top of that. Array pointers (tcb::pointer<T[]>) still deref toslice<T>&orslice<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: