'Why pointer can avoid the warning Warrary-bounds

For the code(Full demo) like:

#include <iostream>

struct A
{
    int a;
    char ch[1];
};

int main() 
{
    volatile A *test = new A;
    test->a = 1;
    test->ch[0] = 'a';
    test->ch[1] = 'b';
    test->ch[2] = 'c';
    test->ch[3] = '\0';
    std::cout << sizeof(*test) << std::endl
              << test->ch[0] << std::endl;
}

I need to ignore the compilation warning like

warning: array subscript 1 is above array bounds of 'volatile char 1' [-Warray-bounds]

which is raised by gcc8.2 compiler:

g++ -O2 -Warray-bounds=2 main.cpp

A method to ignore this warning is to use pointer to operate the four bytes characters like:

#include <iostream>

struct A
{
    int a;
    char ch[1];
};

int main() 
{
    volatile A *test = new A;
    test->a = 1;

    // Use pointer to avoid the warning          
    volatile char *ptr = test->ch;
    *ptr = 'a';
    *(ptr + 1) = 'b';
    *(ptr + 2) = 'c';
    *(ptr + 3) = '\0';
    std::cout << sizeof(*test) << std::endl
              << test->ch[0] << std::endl;
}

But I can not figure out why that works to use pointer instead of subscript array. Is it because pointer do not have boundary checking for which it point to? Can anyone explain that?

Thanks.

Background:

  1. Due to padding and alignment of memory for struct, though ch[1]-ch[3] in struct A is out of declared array boundary, it is still not overflow from memory view
  2. Why don't we just declare the ch to ch[4] in struct A to avoid this warning?
    Answer:
    struct A in our app code is generated by other script while compiling. The design rule for struct in our app is that if we do not know the length of an array, we declare it with one member, place it at the end of the struct, and use another member like int a in struct A to control the array length.
c++


Solution 1:[1]

I want to avoid the warning like

warning: array subscript 1 is above array bounds of 'volatile char 1' [-Warray-bounds]

Well, it is probably better to fix the warning, not just avoid it.

The warning is actually telling you something: what you are doing is undefined behavior. Undefined behavior is really bad (it allows your program to literally anything!) and should be fixed.

Let's look at your struct again:

struct A
{
    int a;
    char ch[1];
};

In C++, your array has only one element in it. The standard only guarantees array elements of 0 through N-1, where N is the size of the array:

[dcl.array]

...If the value of the constant expression is N, the array has N elements numbered 0 to N-1...

So ch only has the elements 0 through 1-1, or elements 0 through 0, which is just element 0. That means accessing ch[1], ch[2] overruns the buffer, which is undefined behavior.

Due to padding and alignment of memory for struct, though ch1-ch3 in struct A is out of declared array boundary, it is still not overflow for memory view, so we want to ignore this warning.

Umm, if you say so. The example you gave only allocated 1 A, so as far as we know, there is still only space for the 1 character. If you do allocate more than 1 A at a time in your real program, then I suppose this is possible. But that's still probably not a good thing to do. Especially since you might run into int a of the next A if you're not careful.

A solution to ignore this warning is to use pointer...But I can not figure out why that works. Is it because pointer do not have boundary checking for which it point?

Probably. That would be my guess too. Pointers can point to anything (including destroyed data or even nothing at all!), so the compiler probably won't check it for you. The compiler may not even have a way of knowing whether the memory you point to is valid or not (or may just not care), and, thus, may not even have a way to warn you, much less will warn you. Its only choice is to trust you, so I'm guessing that's why there's no warning.

Why don't we just declare the ch to ch4 in struct A to avoid this warning?

Side issue: actually std::string is probably a better choice here if you don't know how many characters you want to store in here ahead of time--assuming it's different for every instance of A. Anyway, moving on:

Why don't we just declare the ch to ch4 in struct A to avoid this warning?

Answer:

struct A in our app code is generated by other script while compiling. The design rule for struct in our app is that if we do not know the length of an array, we declare it with one member, place it at the end of the struct, and use another member like int a in struct A to control the array length.

I'm not sure I understand your design principle completely, but it sounds like std::vector might be a better option. Then, size is kept track of automatically by the std::vector, and you know that everything is stored in ch. To access it, it would be something like:

myVec[i].ch[0]

I don't know all your constraints for your situation, but it sounds like a better solution instead of walking the line around undefined behavior. But that's just me.

Finally, I should mention that if you are still really interested in ignoring our advice, then I should mention that you still have the option to turn off the warning, but again, I'd advise not doing that. It'd be better to fix A if you can, or get a better use strategy if you can't.

Solution 2:[2]

There really is no way to work with this cleanly in C++ and iirc the type (a dynamically sized struct) isn't actually properly formed in C++. But you can work with it because compilers still try to preserve compatibility with C. So it works in practice.

You can't have a value of the struct, only references or pointers to it. And they must be allocated by malloc() and released by free(). You can't use new and delete. Below I show you a way that only allows you to allocate pointers to variable sized structs given the desired payload size. This is the tricky bit as sizeof(Buf) will be 16 (and not 8) because Buf::buf must have a unique address. So here we go:

#include <cstddef>
#include <cstdint>
#include <stdlib.h>
#include <new>
#include <iostream>
#include <memory>

struct Buf {
    size_t size {0};
    char buf[];
    [[nodiscard]]
    static Buf * alloc(size_t size) {
        void *mem = malloc(offsetof(Buf, buf) + size);
        if (!mem) throw std::bad_alloc();
        return std::construct_at(reinterpret_cast<Buf*>(mem), AllocGuard{}, size);
    }
    private:
    class AllocGuard {};
    public:
    Buf(AllocGuard, size_t size_) noexcept : size(size_) {}
};

int main() {
    Buf *buf = Buf::alloc(13);
    std::cout << "buffer has size " << buf->size << std::endl;
}

You should delete or implement the assign/copy/move constructors and operators as desired. A another good idea would be to use std::uniq_ptr or std::shared_ptr with a Deleter that calls free() instead of returning a naked pointer. But I leave that as exercise to the reader.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Goswin von Brederlow