I'm trying to wrap my head around the reachability precondition of std::launder
in the context of (abstract) c++ pointer model. Here's a code snippet that makes me confused as to whether this precondition is satified:
int x = 1;
int* px = &x; // px is an int* pointer with value "pointer to x"
void* pr = reinterpret_cast<void*>(&x); // p is a void* pointer with value "pointer to x"
// Convert pr pointer to an integral value and print it on the console/screen
std::cout << reinterpret_cast<uintptr_t>(p);
// User input deliberately set to the same pointer address as the address displayed
// by std::cout (but from perspective of the compiler it's completely random)
uintptr_t in;
std::cin >> in;
void* qr = reinterpret_cast<void*>(in); // qr is a void* pointer with value "pointer to ?????"
int* qx = reinterpret_cast<int*>(qr); // qx is an int* pointer with value "pointer to ?????"
// Are any of the following lines UB?
int* ql = std::launder(qx);
*ql = 2;
As far as I'm aware all of those code statements are legal (except probably the last two lines). The resulting qx
pointer should point to the same address as the original px
pointer. But the question is, does the qx
pointer meet reachability criteria of std::launder
? From the perspective of a compiler (abstract pointer model) we can't reason about whether qx
actually points to a valid object since it's obtained by (potentially) random user input. But from the perspective of the actual user we know that the input address is always the same as the underlying address of object x
. So which of these perspectives are we supposed to take into consideration when analyzing reachability of a source qx
pointer?
Cppreference has some examples on what reachability means in practice by listing a few examples, one of them being the following:
int x2[2][10];
auto p2 = std::launder(reinterpret_cast<int(*)[10]>(&x2[0][0]));
// Undefined behavior: x2[1] would be reachable through the resulting pointer to x2[0]
// but is not reachable from the source
This makes sense since the argument passed to std::launder
is of type int(*)[10]
and value "pointer to x2[0][0]
object", but bytes reachable from the x2[0][0]
object only include the sub-array x2[0]
whereas the resulting (laundered) pointer would have reachability of the entire x2
array (as per the definition mentioning the inclusion of immediately-enclosing arrays). But what would happen if we did the same trick as described at the beginning:
int x2[2][10]
uintptr_t q0 = reinterpret_cast<uintptr_t>(&x2[0][0]);
uintptr_t q1 = reinterpret_cast<uintptr_t>(&x2[0]);
assert(q0 == q1)
// The above assertion is true since the first element of an array has the same
// address as the entire array object
uintptr_t pr = display_and_input(q0);
// Same trick as above with std::cout and std::cin using a q0 address value (or q1
// which is equal to it in value)
auto p2 = std::launder(reinterpret_cast<int(*)[10]>(pr));
// Is it still undefined behavior? We know that q1 address points to valid int[10]
// object (contained within a int[2][10] object) but it's not known from the
// perspective of abstract pointer model or the compiler
I'd imagine that the approach above still results in undefined behavior, but why is that? As far as I can see:
1. reinterpret_cast
guarantees that "A value of any integral or enumeration type can be converted to a pointer type. A pointer converted to an integer of sufficient size and back to the same pointer type is guaranteed to have its original value, otherwise the resulting pointer cannot be dereferenced safely" and here we're technically doing exactly that (pointer -> uintptr_t -> pointer
).
2. There isn't any weird type punning going on and all pointers point to valid addresses where the corresponding typed objects do, in fact, exist
3. The arguments passed to std::launder
are, after reinterpret_cast
'ing, all pointing to valid objects at valid address values which would (in theory) satisfy the reachability condition
I know I'm probably missing some important concept here but what is it exactly? Is it something to do with how pointers are modeled in relation to the standard specification which makes all the points above moot? Or is it something else hard-written in the standard that I'm not aware of?
The reachability question here is really something of a red herring, and std::launder
is never needed (though it is harmless) in these contexts. To determine reachability, we need to know what pointer value is represented by your ?????
s, and it turns out that that is all we need to know.
In the first example, it might be possible to specify that qr
is some unspecified pointer value that is defined to have the needed address, but we already need to do better than that because of ambiguous cases like
#include<cstdint>
int x,y; // reverse on some implementations
bool user();
int main() {
const auto xp=reinterpret_cast<std::uintptr_t>(&x+1),
yp=reinterpret_cast<std::uintptr_t>(&y);
if(xp==yp)
reinterpret_cast<int*>(xp+(yp-xp))[user() ? -1 : 0]=1;
return x+y;
}
Here we don't have an excuse to require std::launder
, because we are converting directly back from std::uintptr_t
and should produce "its original value". The problem of course is that (on typical implementations where xp==yp
) there are two original values (&x+1
and &y
) that are not equivalent for dereferencing and the standard provides no rule to pick one. Here the program uses external input to decide which pointer value it needs, and the only way for the specification to give the program defined behavior is to say that you get whichever one you need.
A current paper of mine addresses this situation by invoking "angelic nondeterminism", where operations like the last reinterpret_cast
above produce whatever value is needed to avoid undefined behavior (so long as such a value exists). The paper also adjusts the definition of important terms like "trivially copyable" precisely because of the unfortunate alternative of either imbuing all data with provenance or producing paradoxical behavior from bitwise copies. The C committee has been working on a similar idea where the equivalent cast produces a pointer value that can be used as if it were &x+1
or &y
but only ever in one such fashion.
If we're going to support such cases, there's no point in suggesting that qr
somehow has the right address but a potentially wrong pointer value that might need std::launder
. As for the unknown user input, the rule is that the program must avoid undefined operations for any choices of unspecified behavior (e.g., the addresses at which objects are placed) but only for actual choices of input. The standard doesn't explicitly address the situation of input to the program that is contingent on its prior output, but a recent paper of mine causes the output of reinterpret_cast<uintptr_t>(p)
to constrain the execution such that user input that "happens" to provide the correct value must work with any reasonable semantics for mid-execution input. The compiler does not need impossible knowledge about the correctness of the user input; it must merely generate a program image whose behavior is correct in that case. (For many other inputs, the program's behavior is unconstrained.)
In the second example, the assertion can fail: the implementation is not required to produce the same std::uintptr_t
value even from the same pointer value twice, let alone different pointer values that have the same address. However, in the typical case where the assertion passes, yes (with my paper or the WG14 proposal) you can use display_and_input
to obtain a pointer to either level of the array. You nonetheless need the assertion (or some other test) guarding the round-trip cast: since the implementation is allowed to produce more interesting integer versions of pointers, it is welcome to declare that in any case where you did not check the equality you do not get the "other" pointer value that you want and you suffer undefined behavior when you try to use it.
This threat is important for optimization, since it gives force to the restriction that an array and its first element are not pointer-interconvertible even though they have the same address: any code that has observed that the integer versions are identical has done the equivalent of having the outer pointer escape such that the optimizations based on the restricted range of the inner pointer are already unavailable. (The implementation does not need to ever actually produce such an interesting integer: it's sufficient to document an unspecified choice as part of the implementation-defined mapping, since either the program has undefined behavior thanks to that unspecified choice or the implementation can choose the boring answer for the actual execution.)