P3852R3: constexpr is_pointer_within memory area
Currently there is no way to check in a standard way if a pointer lies within specific memory area without running into implementation specific behaviour. During constant evaluation any comparison of pointers can lead to failure to constant evaluate in case the pointers are unrelated and points to different top-level objects.
This paper proposes functionality which doesn't propose total ordering of pointers. This paper allows you to check if the pointers are related and in a range, it needs compiler magic to do so.
TL;DR
struct foo {
int id;
int value;
};
int * value_ptr = ...;
foo foo_buffer[10];
int integer_buffer[10];
// safely decide to which buffer the `value_ptr` belongs to
bool simple = std::is_pointer_within(value_ptr, &integer_buffer[0], &integer_buffer[10]);
bool currently_impossible = std::is_pointer_within(value_ptr, &foo_buffer[0], &foo_buffer[10]); // maybe it belongs to suboject!
Changes
R2 → R3: disambiguite cases in the huge table, add fixed restrict example, added comment in implementation about preconditon checking, added discussion why not consteval pointer_is_related functionality
R1 → R2: more explanation why the change should be [first, last) and not inclusive. Added one more motivational example. Update wording to make it Constant When if precondition fails, to avoid injecting UB to the constant evaluation. Added discussion why the past-the-end pointers are not desirable. Added more discussion why span is not preferred.
R0 → R1: added discussion about possibility on some architectures to return false positive, added precondition ptr is not a past-the-end pointer. Rename the function from is_pointer_between to is_pointer_within.
Motivation
At first maybe this does seem as somehow arcane or esoteric. But this is a problem worth solving and it will make C++ a bit more expressive.
does the pointer points to my allocator?
Most typical use-case is to ask "I have a pointer, does it belong to one of my allocations?"
struct allocator {
struct block_type {
const T * first;
const T * last;
const T * begin() const {
return first;
}
const T * end() const {
return last;
}
};
std::vector<block_type> blocks;
constexpr bool belongs_to_me(const T * ptr) const {
// assuming valid pointer
for (const auto & block: blocks) {
// BEFORE: unspecified in a runtime execution or not a constant expression
// if `ptr` points to different top-level object than `block.ptr`
if (block.begin() <= ptr && ptr < block.end()) return true;
// BEFORE but author knows the standard: no longer unspecified (now it is implementation defined) but still not a constant expression
if (std::less_equal<>(block.begin(), ptr) && std::less<>(ptr, block.end())) return true;
// BEFORE but author needs it to work in constant evaluation and she pays the price
if consteval {
if (std::less_equal<>(block.begin(), ptr) && std::less<>(ptr, block.end())) return true;
} else {
// whops, doesn't work for subobjects of `obj` !!!
for (const T & obj: block) if (std::addressof(obj) == ptr) return true;
}
// AFTER: false if `ptr` is not pointing to same top-level object as `block.ptr`
if (is_pointer_within(ptr, block.begin(), block.end())) return true;
}
return false;
}
}
This example shows we don't need to define total ordering of memory, but we just need to have answer "if these pointers are related, how they compare". This paper doesn't propose function bool pointers_related(const void *, const void *) which won't be implementable without additional cost in runtime. And because defining total ordering is also not a possible option, as some architectures wouldn't be able to be conforming, this proposal proposes this function which combines these questions together in one simple low-level functionality.
why not consteval only pointers_related?
Jan argued in mailing list, to provide this (not proposed) function to be consteval only and have it to be magic as is_within_lifetime. I disagree with it, because I don't want to force user to write if consteval in user code. Actually my design criterium for all constexpr functionality is to avoid such need at all. The typical code user would need to write would be:
constexpr bool user_is_pointer_within(const void * ptr, const void * first, const void * last) {
if consteval {
assert(is_pointer_related(first, last));
if (!is_pointer_related(ptr, first) || !is_pointer_related(ptr, last)) {
return false;
}
}
return first <= ptr && ptr < last;
}
Which is basically the proposed functionality of this paper, but without additional benefits of informing optimizer about the relationship between ptr and the memory range. With the same footgun in shape of the past-the-end iterator (if we allow it) and it won't provide any protection in runtime (a static analysis tools, can use the information about the standard library functionality proposed here easily.)
defining total ordering is not feasible
Currently result of a comparison of two unrelated pointers is not specified in the language (according [expr.rel], but you can use std::less<T *> in the library [comparisons] for an implementation specified ordering). This proposal gives you ability to express a different thing than ordering: is something related AND belongs inside? That's in my experience more useful than global total ordering.
But what if you really want the total ordering for pointers? After all most of architectures are like that. For example CHERI is different.
can we make std::less<T *> work in constant evaluator
(Some) constant evaluators doesn't use real pointers for interpreted pointers, in order to being able to detect crimes against abstract machine and UB, (at least in clang) they consider of identifier of top-level object and provenance path. Unrelated objects there are just that ... unrelated symbols.
Hence std::less<T *> can't give us answer as there is no answer about relationship. But for proposed functionality the answer exists.
static analysis and contracts
With this proposal we can specify relationship belongs inside to be usable for contracts and static analysis tools. For example we will be able to specify post contract "returned reference belongs to span you provided" without UB or implementation specific behavior.
constexpr std::hive
For another example, std::hive's member function get_iterator(T *) -> iterator checkes all allocated blocks if provided pointer is in range, and then uses the right block's first member to calculate object's index to construct hive's iterator.
To make this constexpr we must have a way how to check without possibility of a failure to constant evaluate.
Proposed function
constexpr bool is_pointer_within(const void * p, const void * first, const void * last);
If all pointers points to a same outermost enclosing object and first <= p and p < last result is true otherwise it's false.
What can happen?
Following table explores existing options and proposed variants, and why the exclusive is_pointer_within with precondition to disallow a past-the-end pointers is picked:
| Range | Pointer address | Relation | Before | is_pointer_within |
Note | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
language operators |
library comparison |
per-item equality comparison |
inclusive | exclusive and with past-the-end | |||||||
inclusive |
exclusive |
inclusive |
exclusive |
allowed | not allowed | ||||||
first |
ptr < first |
before | false |
false |
false |
false |
false |
false |
|||
first <= ptr |
inside | true |
true |
true |
true |
true |
true |
||||
first <= ptr |
inside with different types |
true |
true |
depends on subobject's offset | true |
true |
true |
||||
last == ptr |
next subobject | true |
false |
true |
false |
false |
true |
false |
false |
logical past-the-end pointer, source of off-by-one errors |
|
last == ptr |
pointer to past the end |
true |
false |
true |
false |
false |
true |
false |
not allowed | ||
last < ptr |
after | false |
false |
false |
false |
false |
false |
||||
first(empty) |
before empty | ptr < first |
false |
false |
false |
false |
false |
false |
|||
last == ptr |
next subobject | true |
false |
true |
false |
false |
true |
false |
false |
logical past-the-end pointer, source of off-by-one errors |
|
last == ptr |
pointer to past the end | false |
true |
false |
true |
false |
true |
false |
not allowed | ||
last < ptr |
after | false |
false |
false |
false |
false |
false |
||||
ptrand first<=lastrange are unrelated |
unrelated | 🔥 unspecified 🔥 | false(not CE) |
false |
false |
false |
false |
the objective of this proposal | |||
next(&pointee) |
pointer past the end (on stack, or in an allocator) |
true(not CE) |
false |
true / false(difference in CE) |
true / false(difference in CE) |
not allowed | without disallowing the past-the-end pointers the answer is different between runtime and a constant evaluation |
||||
next(&other) |
range's past-the-end pointer has same representation as ptr |
true(not CE) |
false(not CE) |
false |
true / false(difference in CE) |
false |
false |
by making it inclusive, it would return different answer in runtime and during constant evaluation |
|||
What the table shows
In the previous table you can see what is proposed in light blue color. Brick-red color is used to show potentially problematic cases (source of off-by-one errors mostly), red color is used to mark dangerous or unspecified cases. The gray color is used to mark things which can't be constant evaluated at all. The green color is used to show improvements.
And the yellow color is used to mark important cases, mapping what allowing / disallowing of the past-the-end pointer values in first argument will result in. It shows that the functionality is not lost, but if someone really wants to check for past-the-end, they can just use equality comparison.
The split between red and green color, shows cases where runtime and constant evaluator will yield on most platform different answer, and the answer can result in problematic code.
Why not just provide check for related pointers?
This is easy to implemented for constant evaluation, but not for runtime code. I think providing such functionality would provide false sense of security and lead to misuse, therefore I'm not proposing it.
Why not span?
We don't have std::span<const void> and we p pointer can be pointing (or not pointing) to a subject of different type than the range (in case it would be typed) and this would lead to unnecessary template instantiation.
Also requiring span will limit this functionality only for known types. At least the ptr argument must be always void, as the storage type is often known (not in case of malloc, where we can't do pointer aritmetic and get ptr + size, which can be problem in constexpr where we don't have malloc yet or allocators), but the pointer type ptr can be multiple things interacting with same range.
Why it's [first, last) and not inclusive range?
It would be impossible to check for intersection of two ranges sitting next to each other. If there is demand for such functionality there is a possibility for bool is_related_and_equal(const void * a, const void * b);
Take following example:
std::array<int, 20> buffer;
enum class relation {
unrelated,
first_half,
second_half,
};
constexpr relation belongs_to_first_half(const int * ptr) {
const int * first = buffer.data();
const int * first_mid = buffer.data() + (buffer.size() / 2);
const int * last = buffer.data() + buffer.size();
if (is_pointer_within(ptr, first, first_mid)) { // problem: surprising inclusion of [first,first_mid]
return relation::first_half;
} else if (is_pointer_within(ptr, first_mid, last)) {
return relation::second_half;
} else {
return relation::unrelated;
}
}
If user pass ptr pointing to &buffer[10] which is in second half, it will get answer relation::first_half. And user will be forced to write something like buffer.data() + (buffer.size() / 2) - 1 and this can be very error-prone as every off-by-one error, whole library is using [first, last) this function should too.
And if you really want to check if ptr is past-the-end pointer to a range, you can safely do ptr == last which will give you true or false without failure to constant evaluate.
This signature seems error prone
I know, I was thinking if it should first, last, pointer, but without span<const void> or providing something like struct memory_range { const void * start; const void * end; }; we will end up with three pointers anyway.
For me the most obvious order is to put them in expected order which will is the one which will return true, anything else would be confusing and users will be wondering if it was first, last, pointer or pointer, first, last, and will end up googling it, trying to open first link to CPlusPlus.com, noticing they are opening it, and quickly go back to open C++ Reference instead and still ends up waiting for fonts to load.
Symmetry with clamp
Originally I had it first, ptr, last, but was told we have std::clamp(ptr, first, last), so I'm mirroring existing signature. I don't want users to franctically google how it is supposed to be.
std::less<T *>
Library specified implementation specific strict total ordering for std::less<T *>, but this is not a case for constant evaluated code.
Weird or segmented architectures
On some platforms it's possible that the comparison return false positive, if somehow pointers points at the same memory but in different segments. This is already happening when someone tries to implement this with std::less<T *> and it's not a problem in practice as such architectures are rare (16-bit x86 mode, ESA/390, z/Architecture).
Passing the past-the-end pointer as first argument
In first revision I had following example which assumed I can pass the past-the-end pointer as first argument. If we allow it, we can do this:
constexpr bool intersect(std::span<const T> lhs, std::span<const T> rhs) noexcept {
const auto * a = lhs.data();
const auto * b = lhs.data() + lhs.size();
const auto * x = rhs.data();
const auto * y = rhs.data() + rhs.size();
// following code assumes we can pass past-the-end pointer as first argument
return std::is_pointer_within(x, a, b)
|| std::is_pointer_within(y, a, b)
|| std::is_pointer_within(a, x, y)
|| std::is_pointer_within(b, x, y);
}
void transfer(std::span<const T> source, std::span<T> target)
pre(not intersect(source, target) && source.size() == target.size())
{
std::copy(source.begin(), source.end(), target.begin());
}
Fixed version of the restrict example
I was asked to provide correct version, as I messed it up (I'm keeping it there for posterity.) And with this version you can see, you actualy don't need the past-the-end pointer (Thanks Barry and Lénard to nudge me about this).
constexpr bool intersect(std::span<const T> lhs, std::span<const T> rhs) noexcept {
const auto * a = lhs.data();
const auto * b = lhs.data() + lhs.size();
const auto * x = rhs.data();
const auto * y = rhs.data() + rhs.size();
// works with exclusive version and doesn't need to support past-the-end pointers
return std::is_pointer_within(x, a, b)
|| std::is_pointer_within(a, x, y);
}
Why it's a bad idea to allow past-the-end pointer
During runtime execution passing past-the-end pointer a stack variable or an object allocated on an allocator have same address representation as other object, and if this function is called to check if points within the other object, it can result in false positive.
During constant evaluation it's trivial to detect it, but that would lead to difference between runtime and constant evaluation:
constexpr bool test(int offset) {
std::array<char, 32> a = {...};
std::array<char, 32> b = {...};
// following line returns different result during runtime/constant evaluation
// if we allow passing past-the-end pointer
return std::is_pointer_within(a.data() + a.size(), b.data(), b.data() + b.size());
}
What's the best answer? Do we want to lie to to user sometimes or prevent mistakes? We can widen it later, but we can't narrow it.
Problems with allowing past-the-end ptr (even if ptr == last)
Imagine a scenario where someone writes allocator similar to example at the top of paper, they will test it with one buffer allocated, and it works, even if the pointer is past-the-end (let's pretend we allowed it). They will assume it will always work, maybe they even write tests in constant evaluator, so they know there is no unspecified behaviour.
In runtime it will work, unless randomly the address of one buffer will be next to other, and suddenly the check returns our past-the-end pointer is first member of other buffer. Crash, at best, it will be a very tricky error to debug. So that's why it must be precondition.
Some asked me to allow it only for ptr == last situation, but that means if we passed ptr and check it against multiple of buffers, the only one which it belongs it will work, return false, because the check is exclusive. But if it tries different buffer, suddenly we have precondition violation (and in constant evaluation a failure to evaluate) and I think this is really bad, because just because we can compare pointers in that one situation, it doesn't mean we should, because it will just teach the user that they can do it, and suddenly later they will be surprised it fails with multiple ranges.
| Situation | Number of buffers | past-the-end pointer? | ||
|---|---|---|---|---|
| Allowed | Allowed only for (ptr == last) | Disallowed | ||
| checking valid pointer belonging to our buffer | 1+ | correct true on the correct buffer | ||
| checking a past-the-end pointer | 1 | correct answer false | precondition violation | |
| checking past-the-end pointer belonging to our buffer | 2+ | sometimes invalid answer* in runtime, correct false in constant evaluation | valid answer false or precondition violation* | |
| checking foreign past-the-end pointer | 2+ | precondition violation | ||
As you can see in the table, once you have multiple buffers, and they are sequentielly queried, allowing past-the-end pointer can lead to sometimes irrecoverable precondition failure or random errors, which are hard to debug. Instead of optimizing of case of one buffer, I prefer the proposed functionality to be predictable.
Implementation
A prototype in Clang's ExprConstant.cpp and the new bytecode interpreter, basically it needs a magic (consteval) compiler builtin to check if objects are related and use it to avoid failure which can't be recovered:
constexpr bool is_pointer_within(const void * ptr, const void * first, const void * last) pre(!__builtin_is_past_the_end_pointer(ptr)) {
// NOTE: this is a prototype implementation from me as a frontend person,
// a proper implementation should be done as whole builtin,
// and propagate the information to optimizer
if consteval {
// assuming existence of such builtin constant evaluation pointers
// has all metadata needed to make this check easy
if (!__builtin_pointer_related(first, ptr) || !__builtin_pointer_related(ptr, last)) {
return false;
}
}
return first <= ptr && ptr < last;
}
Why the implementation doesn't check for the precondition?
Library preconditions often are not check, and this is the same case, as sometimes it's impossible to check, but still is better to say "if you pass past-the-end pointer, it won't work".
Alternative way how to get almost the same result
In cases where we have an array (or a set) of items, and wants to check if the item is inside array, I can just check against every single one for equality which is properly defined (unless one of the pointers is past-the-end pointer).
template <typename T> constexpr bool is_pointer_within(std::span<const T> array, const T * p) {
// doesn't check past-end pointer
return std::ranges::any_of(array, [=](const T & obj) { return std::addressof(obj) == p; });
}
Is this okay? It's not, it's O(n). Any such operation shouldn't be this slow. Also it doesn't allow us to check if subject is in the range as a subject, so the functionality is actually different.
And the major usability problem is, it doesn't work with subobjects with non-zero offset.
Wording
Add following to [memory.syn]. Note wording is using what [expr.const] is already doing with comparison, and because objective is to avoid such failure I used same wording construct.
// [ptr.within], check if a pointer belongs to a specific memory range
constexpr bool is_pointer_within(const void * ptr, const void * first, const void * last); // freestanding
Add this somewhere in [memory] preferably after [ptr.align].
20.2.5 Pointer within pointers [ptr.within]
constexpr bool is_pointer_within(const void * ptr, const void * first, const void * last);
Preconditions: result of comparison first <= last is specified and is true ([expr.rel]) and ptr is not a past-the-end pointer.
Constant When: Preconditions are met.
Returns: true if comparisons first <= ptr and ptr < last are both specified and both are evaluated as true; otherwise false.
Feature test macro
17.3.2 Header <version> synopsis [version.syn]
#define __cpp_lib_is_pointer_within 2026??L // freestanding, also in <memory>