5

A code that I wrote was warning-free in GCC 4.9, GCC 5 and GCC 6. It was also warning-free with some older GCC 7 experimental snapshots (for example 7-20170409). But in the most recent snapshot (including the first RC), it started to produce a warning about aliasing. The code basically boils down to this:

#include <type_traits>

std::aligned_storage<sizeof(int), alignof(int)>::type storage;

int main()
{
    *reinterpret_cast<int*>(&storage) = 42;
}

Compilation with latest GCC 7 RC:

$ g++ -Wall -O2 -c main.cpp
main.cpp: In function 'int main()':
main.cpp:7:34: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
  *reinterpret_cast<int*>(&storage) = 42;

(interesting observation is that the warning is not produced when optimizations are disabled)

Compilation with GCC 6 gives no warnings at all.

Now I'm wondering, the code above definitely HAS type-punning, no question about that, but isn't std::aligned_storage meant to be used that way?

For instance the example code given here generally produces no warning with GCC 7 but only because:

  • std::string somehow is not affected,
  • std::aligned_storage is accessed with an offset.

By changing std::string into int, removing offset access to std::aligned_storage and removing irrelevant parts you get this:

#include <iostream>
#include <type_traits>
#include <string>

template<class T, std::size_t N>
class static_vector
{
    // properly aligned uninitialized storage for N T's
    typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
    std::size_t m_size = 0;

public:

    // Access an object in aligned storage
    const T& operator[](std::size_t pos) const
    {
        return *reinterpret_cast<const T*>(data/*+pos*/); // <- note here, offset access disabled
    }
};

int main()
{
    static_vector<int, 10> v1;
    std::cout << v1[0] << '\n' << v1[1] << '\n';
}

And this produces exactly the same warning:

main.cpp: In instantiation of 'const T& static_vector<T, N>::operator[](std::size_t) const [with T = int; unsigned int N = 10; std::size_t = unsigned int]':
main.cpp:24:22:   required from here
main.cpp:17:16: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
         return *reinterpret_cast<const T*>(data/*+pos*/);
                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

So my question is - is this a bug or a feature?

iehrlich
  • 3,572
  • 4
  • 34
  • 43
Freddie Chopin
  • 8,440
  • 2
  • 28
  • 58
  • 3
    You wouldn't use `aligned_storage` like that. Rather, you would use `int * p = new (&storage) int(42);`. – Kerrek SB Apr 30 '17 at 21:36
  • @KerrekSB - please note that the example from cppreference does exactly the same thing that I did. – Freddie Chopin Apr 30 '17 at 21:38
  • Hm. Never trust a wiki you haven't edited yourself? – Kerrek SB Apr 30 '17 at 21:48
  • @KerrekSB - It's not a question of trust, I'm just trying to determine whether this is intended behaviour of GCC. The fact that cppreference did the same thing just proves that I'm not the only one who wanted to use it that way (; Sure, placement new would be better, but actually in my real use case it would needlessly complicate things. – Freddie Chopin Apr 30 '17 at 21:55
  • 1
    The example from cppreference's page on [std::aligned_storage](http://en.cppreference.com/w/cpp/types/aligned_storage) does the placement new. Are you talking of another page? – Cubbi Apr 30 '17 at 23:30
  • I'm curious if the warning goes away if you put the offset back in: `return *reinterpret_cast(data+pos);` – Michael Burr May 01 '17 at 02:03
  • @Cubbi: the cppreference example does placement new in the `emplace_back()`, but does a `reinterpret_cast` in the `operator[]()`. The example posted here only has the `operator[]()` part of the example. – Michael Burr May 01 '17 at 02:14
  • @MichaelBurr - yes - the warning is gone when there is an "unknown" offset or an offset other than 0. With `...+pos`, `...+1`, `...-1` or `...+1234` there is no warning, but with `...+pos-pos` or `...+0` (or no offset at all) the warning is back. – Freddie Chopin May 01 '17 at 06:42
  • What happens if you do: `return *reinterpret_cast(&data[0]);`? – Michael Burr May 01 '17 at 14:14
  • @MichaelBurr - it would be too easy (; The warning is still there. Moreover - even if the offset is non-zero (`(&data[1])`) or "unknown" (`(&data[pos])`) the warning stays. – Freddie Chopin May 01 '17 at 17:38
  • @FreddieChopin: I was ultimately able to test that for myself (I don't have GCC 7 installed, but I realized that gcc.godbolt.org does). – Michael Burr May 01 '17 at 18:38

2 Answers2

3

I can't answer whether or not there really is a potential for undefined behavior due to aliasing or if the warning is unwarranted. I find the aliasing topic to be a rather complex minefield.

However, I think that the following variation of your code eliminates the aliasing problem without any overhead (and perhaps is more readable).

#include <iostream>
#include <type_traits>
#include <string>

template<class T, std::size_t N>
class static_vector
{
    // properly aligned uninitialized storage for N T's
    union storage_t_ {
        T item;
        typename std::aligned_storage<sizeof(T), alignof(T)>::type aligned_member;
    };
    storage_t_ data[N];

    std::size_t m_size = 0;

public:

    // Access an object in aligned storage
    const T& operator[](std::size_t pos) const
    {
        return data[0].item;
    }
};

int main()
{
    static_vector<int, 10> v1;
    std::cout << v1[0] << '\n' << v1[1] << '\n';
}

Whether it's acceptable for your situation, I can't be sure.

Michael Burr
  • 333,147
  • 50
  • 533
  • 760
  • This is useless if you want to over-align your storage, e.g. for SIMD. Changing `alignof(T)>::type` to `16` for example changes the size of `static_vector v1;` from `48 = 10*4 + sizeof(size_t)` to `176 = 10*16 + size_t + padding`. So you don't get an aligned array, you get an array of aligned/padded elements. If you only support using the default alignment of `T` then I don't see the point, `T data[N];` already does that. – Peter Cordes Jan 15 '19 at 06:30
  • Oh, I guess `std::aligned_storage` is useful for types with default constructors where you want to delay construction. [For which purposes needs std::aligned\_storage?](https://stackoverflow.com/q/50271304). If you want over-alignment for SIMD, `alignas(32) float foo[N]` is still good, but I was hoping `std::aligned_storage` would let us portably declare that a function accepts a pointer to an aligned-by-16 array of `float`, for example. Instead of GNU-only stuff like [`__builtin_assume_aligned` or a GCC-only typedef of aligned float](https://stackoverflow.com/q/54189780/224132) – Peter Cordes Jan 15 '19 at 06:40
1

Your code causes undefined behaviour (although the warning text is a bit tangential to the underlying cause). In C++ the concepts storage and objects are different things. Objects occupy storage; but storage may exist without objects in it.

The aligned_storage mechanism provides storage with no objects in it. You may create objects in it by using placement-new. However your code uses the assignment operator on storage that does not contain any objects. If you consult the definition of the assignment operator you'll find that it has no provision for creating an object; and in fact it only defines what happens when the left-hand side designates an object that already exists.

The code in your main should be:

new(&storage) int(42);

Note that since we are working with a primitive type here, it's not required to do any form of destructor call, and you can call placement-new multiple times on the same space with no issue.

The section [basic.life] of the standard talks about what you can do with storage that doesn't contain objects, and what happens if you use placement-new or destructor calls on objects that do exist within storage.

See also this answer.


The code in the cppreference aligned_storage is correct. You provide some incorrect code based on that which you described as "removing irrelevant parts", however you removed a very relevant part which was the call to placement-new to create objects in the storage:

new(data+m_size) T(std::forward<Args>(args)...);

Then it is correct to write return *reinterpret_cast<const T*>(data+pos); when pos is a valid index, and the expression accesses the object created by an earlier placement-new call.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • I'm down voting because even though all of this is technically correct, it doesn't really answer the question of why is gcc emitting a warning (unless you're calling out buggy behavior in gcc 7.1 without saying exactly that). `new (&storage) int(42); *reinterpret_cast(&storage) = 43;` [still has the same warning](https://godbolt.org/g/2FvjHY) (hover over the green squiggles). – zneak Jul 01 '17 at 01:57
  • @zneak OP clearly thinks that their code is correct, so I think it is relevant to explain why the code is undefined behaviour. IMO code that is UB and produces a warning is not sufficient evidence that the warning is a bug. In fact the warning is technically correct for OP's code, and turned out to be somewhat useful in that it identified a problem. – M.M Jul 01 '17 at 02:17
  • Your comment does appear to demonstrate a compiler bug and perhaps could be posted as a new question. The bugfix might be to remove the warning for your code, and retain a warning (perhaps worded differently) for OP's code. – M.M Jul 01 '17 at 02:17
  • 1
    Do note that the behaviour which I described in the first comment was indeed a GCC bug, now fixed. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80593 – Freddie Chopin Jul 05 '17 at 13:43
  • You remark about the example from cppreference is wrong - whether or not there is a placement new somewhere else doesn't change the fact that the reinterpret_cast produces a warning, while it did not produce it in earlier GCC versions. – Freddie Chopin Jul 05 '17 at 13:44
  • The one where you write about placement new being "very relevant", while it is completely irrelevant to the problem, as GCC 7.1 will emit a warning no matter if you used placement new or not. Generally this answer tries to answer a different question, because I was not asking for a correct way to construct an object in the storage, just for the clarification whether GCC's behaviour is a bug or a feature. Finally it turned out to be a bug. – Freddie Chopin Jul 07 '17 at 11:00
  • placement-new is relevant to whether the code is correct or not. I already stated my position: if the code is undefined behaviour, then it's not a compiler bug if the compiler warns about the mistake. Which it did. – M.M Jul 07 '17 at 11:39
  • Surprisingly GCC developers thought this was a bug in GCC, and decided to fix that... I'm not sure you've read what I wrote. Take the WHOLE example from cppreference, change string to int, remove offset and compile it with GCC7. You _will_ get a warning about your "correct" code. If you now remove the placement new you will still get the warning. That's why I consider this part to be _not_ relevant to the problem. You think otherwise. Fine. Doesn't change anything - I just wanted my question to be shorter. – Freddie Chopin Jul 07 '17 at 16:10