Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does GCC optimize out assignments here?

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

  • the assigned value is used directly in the function itself
  • the offset_ptr is located on the heap, not on the stack
  • no offset_ptr is used at all

It works for:

  • Clang (with and without optimizations)
  • MSVC (Debug and Release mode)
  • GCC (current as well as old versions) -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.

like image 624
Felix Gündling Avatar asked May 12 '26 22:05

Felix Gündling


2 Answers

[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] and x[j] of the same array object x, the expression P - Q has the value i-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.

like image 163
Rakete1111 Avatar answered May 14 '26 12:05

Rakete1111


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

like image 40
geza Avatar answered May 14 '26 13:05

geza



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!