I have a class offset_ptr that works like a pointer but stores the memory address it points to as offset to its own address this. Here is a version with everything removed that's not required to demonstrate the problem:
template <typename T>
struct offset_ptr {
using offset_t = int64_t;
static constexpr auto const NULLPTR_OFFSET =
std::numeric_limits<offset_t>::max();
offset_ptr(T const* p)
: offset_{p == nullptr ? NULLPTR_OFFSET
: static_cast<offset_t>(
reinterpret_cast<uint8_t const*>(p) -
reinterpret_cast<uint8_t const*>(this))} {}
T* get() {
return
offset_ == NULLPTR_OFFSET
? nullptr
: reinterpret_cast<T*>(reinterpret_cast<uint8_t*>(this) + offset_);
}
offset_t offset_;
};
This code does not work with GCC -O2 and -O3:
int* get() {
offset_ptr<int> ptr = static_cast<int*>(malloc(sizeof(int)));
auto p = ptr.get();
*p = 110; // WOW - please do not optimize me away :-(
return p;
}
(memory management and error checking intentionally omitted to keep it simple!)
This is also visible in the generated assembly: https://godbolt.org/z/PfZEJM
The assignment is just missing.
As shown in the Godbolt Compiler Explorer link above it works when
offset_ptr is located on the heap, not on the stackoffset_ptr is used at allIt works for:
-O0 and -O1 (but NOT for -O2 and -O3)GCC and Clang Address and UB sanitizer builds do not indicate any problems (besides the leaked memory) when executed.
Can someone point out a section in the C++ standard document that says that there is UB in this code (which could be the reason for GCC aggressively optimizing out the assignment)? Or is it a bug in GCC?
Edit:
Removing the nullptr checks in offset_ptr helps (https://godbolt.org/z/5HjcLY). But I need those null-checks.
[expr.add]p5:
When two pointer expressions P and Q are subtracted, the type of the result is an implementation-defined signed integral type; [...]
- If P and Q both evaluate to null pointer values, the result is 0.
- Otherwise, if P and Q point to, respectively, elements
x[i]andx[j]of the same array objectx, the expression P - Q has the valuei-j.- Otherwise, the behavior is undefined.
The subtraction in the member initializer list falls back on the third point so you have UB.
It "works" if you remove the nullptr checks because gcc cannot prove that the first condition doesn't happen.
You can make this work, if you use reinterpret_cast to uintptr_t instead of uint8_t *. This way you trade UB to implementation-defined behavior.
See: https://godbolt.org/z/rBTqYl
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With