Let's say I perform the following:
void g(int* x)
{
int y = 0;
auto diff = uintptr_t(&y) - uintptr_t(x);
}
void f()
{
int x = 0;
g(&x);
}
Does diff merely have undefined value, or does the code invoke undefined behaviour? According to the specification, is the code guaranteed to run nicely and compute a value for diff, possibly meaningless, or does it invoke UB? I believe there's something about unrelated variables, but could not pinpoint it.
I'm interested in answers regarding any standard since (including) C++ 11.
Discussion arose from comments in: Print stack in C++
To quote the C++11 standard draft. On the subject of converting a pointer to an integer
[expr.reinterpret.cast]
5 A value of integral type or enumeration type can be explicitly
converted to a pointer. A pointer converted to an integer of
sufficient size (if any such exists on the implementation) and back to
the same pointer type will have its original value; mappings between
pointers and integers are otherwise implementation-defined.
Since uintptr_t must be defined for the your code to compile, then there exists an integer type on the target machine capable of being the target of the pointer-to-integer conversion. The mapping is implementation defined, but most importantly the result is not indeterminate. This means you obtain some valid integer for both conversions.
So the subtraction is not undefined behavior. But the result is implementation defined.
Converting pointer to integer of sufficient size is well defined, subtracting unsigned integer from another is well defined regardless of their value. There is no undefined behaviour here.
But also, standard doesn't guarantee any particular value for the converted integers, and therefore neither for the result of their subtraction.
Related
Consider this example
int main(){
std::intptr_t value = /* a special integer value */;
int* ptr = reinterpret_cast<int*>(value ); // #1
int v = *ptr; // #2
}
[expr.reinterpret.cast] p5 says
A value of integral type or enumeration type can be explicitly converted to a pointer. A pointer converted to an integer of sufficient size (if any such exists on the implementation) and back to the same pointer type will have its original value; mappings between pointers and integers are otherwise implementation-defined.
At least, step #1 is implementation-defined. For step #2, in my opinion, I think it has four possibilities, which are
The implementation does not support such a conversion, and implementation does anything for it.
The pointer value is exactly the address of an object of type int, the result is well-formed.
The pointer value is the address of an object other than the int type, the result is UB.
The pointer value is an invalid pointer value, the indirection is UB.
It means what the behavior of the indirection through the pointer ptr will be depends on the implementation. It is not definitely UB if the implementation takes option 2. So, I wonder whether this case is not definitely UB or is definitely UB? If it is latter, which provisions strongly state the behavior?
The standard has nothing more to say on it than what you quoted. The standard only guarantees the meaning of a integer-to-pointer cast if that integer value was taken from a pointer-to-integer cast. The meaning of all other integer-to-pointer conversions are implementation defined.
And that means everything about them is "implementation defined": what they result in and what using those results will do. After all, the pointer-to-integer-to-pointer round trip spells out that you get the "original value" back. The fact that the resulting pointer has the "original value" means that it will behave exactly like you had copied the original pointer itself. So the standard needs say nothing more on the matter.
The behavior of the pointer taken from an implementation-defined integer-to-pointer cast is... implementation-defined. If an implementation says that supports such conversions, it must spell out what the result of supported conversions are. That is, if there is some "a special integer value" for which a cast to an int* is supported by the implementation, it must say what the result of that cast is. This includes things like whether it pointer to an actual int or whatever.
Is performing indirection from a pointer acquired from converting an integer value definitely UB?
Not always. Here is an example that is definitely not UB:
int i = 42;
std::intptr_t value = reinterpret_cast<std::intptr_t>(&i);
int* ptr = reinterpret_cast<int*>(value);
int v = *ptr;
This is because converting a pointer to an integer of sufficient size and back to the same pointer type is guaranteed to yield the same pointer value as stated in the rule you quoted. Since the original pointer value was valid for indirection, so is the converted one.
While reading comments of a C++ Weekly video about the constexpr new support in C++20 I found the comment that alleges that C++20 allows UB in constexpr context.
At first I was convinced that comment is right, but more I thought about it more and more I began to suspect that C++20 wording contains some clever language that makes this defined behavior.
Either that all transient allocations return unique addresses or maybe some more general notion in C++ that makes 2 distinct allocation pointers always(even in nonconstexpr context) compare false even if at runtime in reality it is possible that allocator would give you back same address(since you deleted the first allocation).
As a bonus weirdness: you can only use == for comparison, <, > fail...
Here is the program with alleged UB in constexpr:
#include <iostream>
static constexpr bool f()
{
auto p = new int(1);
delete p;
auto q = new int(2);
delete q;
return p == q;
}
int main()
{
constexpr bool res1 = f();
std::cout << res1 << std::endl; // May output 0 or 1
}
godbolt
The result here is implementation-defined. res1 could be false, true, or ill-formed, based on how the implementation wants to define it. And this is just as true for equality comparison as it is for relational comparison.
Both [expr.eq] (for equality) and [expr.rel] (for relational) start by doing an lvalue-to-rvalue conversion on the pointers (because we have to actually read what the value is to do a comparison). [conv.lval]/3 says that the result of that conversion is:
Otherwise, if the object to which the glvalue refers contains an invalid pointer value ([basic.stc.dynamic.deallocation], [basic.stc.dynamic.safety]), the behavior is implementation-defined.
That is the case here: both pointers contain an invalid pointer value, as per [basic.stc.general]/4:
When the end of the duration of a region of storage is reached, the values of all pointers representing the address of any part of that region of storage become invalid pointer values. Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior.
with a footnote reading:
Some implementations might define that copying an invalid pointer value causes a system-generated runtime fault.
So the value we get out of the lvalue-to-rvalue conversion is... implementation-defined. It could be implementation-defined in a way that causes those two pointers to compare equal. It could be implementation-defined in a way that causes those two pointers to compare not equal (as apparently all implementations do). Or it could even be implementation-defined in a way that causes the comparison between those two pointers to be unspecified or undefined behavior.
Notably, [expr.const]/5 (the main rule governing constant expressions), despite rejecting undefined behavior and explicitly rejecting any comparison whose result is unspecified ([expr.const]/5.23), says nothing about a comparison whose result is implementation-defined.
There's no undefined behavior here. Anything goes. Which is admittedly very weird during constant evaluation, where we'd expect to see a stricter set of rules.
Notably, with p < q, it appears that gcc and clang reject the comparison as being not a constant expression (which is... an allowed result) while msvc considers both p < q and p > q to be constant expressions whose value is false (which is... also an allowed result).
int main(){
int v = 1;
char* ptr = reinterpret_cast<char*>(&v);
char r = *ptr; //#1
}
In this snippet, the expression ptr point to an object of type int, as per:
expr.static.cast#13
Otherwise, the pointer value is unchanged by the conversion.
Indirection ptr will result in a glvalue that denotes the object ptr point to, as per
expr.unary#op-1
the result is an lvalue referring to the object or function to which the expression points.
Access an object by using a glvalue of the permitted type does not result in UB, as per
basic.lval#11
If a program attempts to access ([defns.access]) the stored value of an object through a glvalue whose type is not similar ([conv.qual]) to one of the following types the behavior is undefined:
a char, unsigned char, or std::byte type.
It seems it also does not violate the following rule:
expr#pre-4
If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.
Assume the width of char in the test circumstance is 8 bits, its range is [-128, 127]. The value of v is 1. So, Does it mean the snippet at #1 does not result in UB?
As a contrast, given the following example
int main(){
int v = 2147483647; // or any value greater than 127
char* ptr = reinterpret_cast<char*>(&v);
char r = *ptr; //#2
}
#2 would be UB, Right?
It is the intention of the language that both snippets be implementation defined. I believe they were, until to C++17 which broke support for that language feature. See the defect report here. As far as I know, this has not been fixed in C++20.
Currently, the portable workaround for accessing memory representation is to use std::memcpy (example) :
#include <cstring>
char foo(int v){
return *reinterpret_cast<char*>(&v);
}
char bar(int v)
{
char buffer[sizeof(v)];
std::memcpy(buffer, &v, sizeof(v));
return *buffer;
}
foo is technically UB while bar is well defined. The reason is foo is UB is by omission. Anything the standard fails to define is by definition UB and the standard, in its current state, fails to define the behavior of this code.
bar produces the same assembly as foo with gcc 10. For simple cases, the actual copy is optimized out.
Regarding your rational, the reasoning seems sound except that, in my opinion, the rules defining unary operator* (expr.static.cast#13) doesn't have the effect you expect in this case. The pointer must point to the underlying representation, which is poorly defined as the linked defect describes. The fact that the pointer's value doesn't change does not mitigate the fact that it points to a different object. C++ allows objects to have the same address if their types are different, such as the first member in a standard layout class sharing the same address as the owning instance.
Note that the author is the defect report came to the same conclusion as you regarding snippet #1, but I disagree. But due to the fact that we are dealing with a language defect, and one that conflicts with state intentions, it is hard to definitively prove one behavior correct. The fundamental rules these arguments would be based on are known to be flawed in this particular case.
Does it mean the snippet at #1 does not result in UB?
Yes, the quoted rules mean that #1 is well defined.
#2 would be UB, Right?
No, as per the quoted rules, the behaviour of #2 is also well defined.
The type of ptr is char*, therefore the type of the expression *ptr is char whose value cannot exceed the value representable by char, thus expr#pre-4 does not apply.
Assume the width of char in the test circumstance is 8 bits, its range is [-128, 127].
This assumption is not necessary in order for #1 to be well defined.
The value of v is 1
This does not follow from the above assumption alone. It may be practically true in case of a little endian CPU (including the previous assumptions) although the standard doesn't specify the representation exactly.
If you know two pieces of information:
A memory address.
The type of the object stored in that address.
Then you logically have all you need to reference that object:
#include <iostream>
using namespace std;
int main()
{
int x = 1, y = 2;
int* p = (&x) + 1;
if ((long)&y == (long)p)
cout << "p now contains &y\n";
if (*p == y)
cout << "it also dereference to y\n";
}
However, this isn't legal per the C++ standard. It works in several compilers I tried, but it's Undefined Behavior.
The question is: why?
It wreaks havoc with optimizations.
void f(int* x);
int g() {
int x = 1, y = 2;
f(&x);
return y;
}
If you can validly "guess" the address of y from x's address, then the call to f may modify y and so the return statement must reload the value of y from memory.
Now consider a typical function with more local variables and more calls to other functions, where you'd have to save the value of every variable to memory before each call (because the called function may inspect them) and reload them after each call (because the called function may have modified them).
If you want to treat pointers as a numeric type, firstly you need to use std::uintptr_t, not long. That's the first undefined behavior, but not the one you're talking about.
It works in several compilers I tried, but it's Undefined Behavior.
The question is: why?
Okay, so the comments section went off when I called this undefined behavior. It's actually unspecified behavior (a.k.a. implementation defined).
You are trying to compare two distinctly unrelated pointers:
&x + 1
&y
The pointer &x+1 is a one-past-the-end pointer. The standard allows you to have such a pointer, but the behavior is only defined when you use it to compare against pointers based on x. The behavior is not specified if you compare it with anything else: [expr.eq § 3.1]
The compiler is free to put y anywhere it chooses, including in a register. As such, there is no guarantee that &y and &x+1 are related.
As an exercise to someone who wants to show whether this is in fact undefined behavior or not, perhaps start here:
[basic.stc.dynamic.safety § 3.4]:
An integer value is an integer representation of a safely-derived pointer only if its type is at least as large as std::intptr_t and it is one of the following: ...
3.4 the result of an additive or bitwise operation, one of whose operands is an integer representation of a safely-derived pointer value P, if that result converted by reinterpret_cast would compare equal to a safely-derived pointer computable from reinterpret_cast(P).
[basic.compound § 3.4] :
Note: A pointer past the end of an object ([expr.add]) is not considered to point to an unrelated object of the object's type that might be located at that address
If you know address and type of an object and your implementation has relaxed pointer safety [basic.stc.dynamic.safety §4], then it should be legal to just access the object at that address through an appropriate lvalue I think.
The problem is that the standard does not guarantee that local variables of the same type are allocated contiguously with addresses increasing in order of declaration. So you cannot derive the address of y based on that computation you do with the address of x. Apart from that, pointer arithmetic would lead to undefined behavior if you go more than one element past an object ([expr.add]). So while (&x) + 1 is not undefined behavior yet, just the act of even computing (&x) + 2 would be…
The code is legal per the C++ standard (i.e. should compile), but as you already noted the behaviour is undefined. This is because the order of variable declaration does not imply that they will be arranged in memory in the same way.
Consider the following code:
int i = 1;
char c[sizeof (i)];
memcpy(c, &i, sizeof (i));
cout << static_cast<int>(c[0]);
Please ignore whether this is good code. I know the output depends on the endianness of the system. This is only an academic question.
Is this code:
Undefined behaviour
Implementation-defined behaviour
Well-defined behaviour
Something else
The language does not say that doing this is immediately undefined behavior. It simply says that the representation of c[0] might end up being invalid (trap) representation, in which case the behavior is indeed undefined. But in cases when c[0] is not a trap representation, the behavior is implementation-defined.
If you use unsigned char array, trap representation becomes impossible and behavior becomes purely implementation-defined.
The rule you are looking for is 3.9p4:
The object representation of an object of type T is the sequence of N unsigned char objects taken up by the object of type T, where N equals sizeof(T). The value representation of an object is the set of bits that
hold the value of type T. For trivially copyable types, the value representation is a set of bits in the object representation that determines a value, which is one discrete element of an implementation-defined set of values.
So if you use unsigned char, you do get implementation-defined behavior (any conforming implementation must give you a guarantee on what that behavior is).
Reading through char is also legal, but then the values are unspecified. You are however guaranteed that using unqualified char will preserve the value (therefore bare char cannot have trap representations or padding bits), according to 3.9p2:
For any object (other than a base-class subobject) of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes (1.7) making up the object can be copied into an array of char or unsigned char. If the content of the array of char or unsigned char is copied back into the object, the object shall subsequently hold its original value.
("unspecified" values are a bit weaker than "implementation-defined" values -- the semantics are the same but the platform is not required to document what the values are.)
It is clearly implementation defined behaviour.
The internal representation of an int is not defined by the standard (implementations can choose little or big endian or whatever else), so it cannot be well defined behaviour : the result is allowed to be different on different architectures.
On a defined system (architecture and C compiler and (eventually) configuration) the behaviour is perfectly determined : on a big endian, you will get a 1, on a little endian a 0. So it is implementation defined behaviour.