Search code examples
c++gccgcc-warning

GCC bogus "may be used uninitialized " warning starting with version 11


In our codebase we have code that boils down to the following (trying to use dynamic stack allocation instead of heap allocation):

#include <memory>
#include <alloca.h>

void f(long long const* data, size_t len);

void h(int const* data, size_t len)
{
  std::unique_ptr<char[]> heapBuff;

  auto nbytes = len * sizeof(long long);
  long long* arr;                                                 
  if (nbytes <= 1024) {                          
    arr = reinterpret_cast<long long*>(alloca(nbytes));
  }
  else {
    heapBuff.reset(new char[nbytes]);
    arr = reinterpret_cast<long long*>(heapBuff.get());
  }

  for (size_t i=0; i < len; ++i)
    arr[i]=data[i];

  f(arr, len);
}

(complete example in Compiler Explorer), and compilation results in a "may be used uninitialized" warning in GCC starting with version 11.

/app/h.cpp: In function 'void h(const int*, size_t)':
/app/h.cpp:23:4: warning: 'arr' may be used uninitialized [-Wmaybe-uninitialized]
   23 |   f(arr, len);
      |   ~^~~~~~~~~~
In file included from /app/h.cpp:2:
/app/f.hpp:4:6: note: by argument 1 of type 'const long long int*' to 'void f(const long long int*, size_t)' declared here
    4 | void f(long long const* data, size_t len);
      |      ^

Clang or earlier versions of GCC do not produce this warning.
To my eye, there is no path leading to the arr pointer being uninitialized, and the code is so simple that the compiler should have no problem figuring it out. Am I missing something?
One curious thing is that replacing alloca() with malloc() does not affect the warning, but replacing it with some non-inlined void* allocate(size_t) function removes the warning.


Solution

  • First of, I can see six minor issues that are wrong with the code:

    1. Technically new char[nbytes] is not able to implicitly create an array of long long objects nested in the char array. Consequently accessing through the result of reinterpret_cast<long long*> has undefined behavior. This can be easily remedied by replacing char with unsigned char or std::byte. In both cases starting the lifetime of an array of that type also implicitly creates objects (of implicit-lifetime type) nested in the array.

    2. std::unique_ptr::get will return a pointer to the first array element, not a pointer to any nested long long object (assuming any was created implicitly). Therefore you need an extra std::launder call after reinterpret_cast. (There is currently a proposal to change the standard so that this launder call is no longer necessary.)

    3. It is not clear whether alloca also implicitly creates objects. Since this is a non-standard function, the standard obviously has nothing to say about it and I doubt that any documentation will make a determination on that. However, the only behavior that makes sense to me in practice is that it does implicitly create objects. Presumably it then also returns a pointer to such an implicitly-created object.

    4. len * sizeof(long long) can wrap-around when the exact result is larger than the largest representable value of size_t. In that case, in the loop, arr[i]=data[i] will eventually access arr out-of-bounds, causing undefined behavior.

    5. Because alloca is not standardized, it is not obvious that it will return memory suitably aligned for long long. The GCC documentation for __builtin_alloca says that the memory will be aligned to __BIGGEST_ALIGNMENT__, which should be large enough to store all non-overaligned types.

    6. Again, because alloca is not standardized, it is not obvious what it will return if the size argument is 0. Unfortunately the GCC documentation for __builtin_alloca doesn't say. From my testing it seems that the stack pointer is returned, although there was some unusual behavior when trying to return the pointer from the function.

    However, I do not really see any connection to the warning GCC produces. Even simplifying the example to

    void* arr = alloca(10);
    f(arr, 0);
    

    GCC still complains (https://godbolt.org/z/vhnr3EaMr).

    Interestingly, if you store anything to the memory, then GCC stops complaining (https://godbolt.org/z/x31Gacdrb).

    So I suspect that GCC is not happy with the len == 0 case where nothing can be written to the allocated (zero-size) memory. An indication for this is that adding an early return on len == 0 gets rid of the warning.

    And I think the reason that the warning checks whether something is written to the memory is that you pass it as const pointer, which indicates that the function f will (only) read from the memory. If nothing was written to it, then nothing can be read from it. If you make the parameter non-const the warning is gone as well.