GCC 7, align_storage и "разыменование указателя типа-перфоратора нарушит правила строгого наложения имен"

Код, который я написал, был без предупреждений в GCC 4.9, GCC 5 и GCC 6. Он также был без предупреждений с некоторыми более ранними экспериментальными снимками GCC 7 (например, 7-20170409). Но в самом последнем снимке (включая первый RC) он начал выдавать предупреждение о псевдонимах. Код в основном сводится к следующему:

#include <type_traits>

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

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

Компиляция с последним 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;

(интересное наблюдение заключается в том, что предупреждение не выдается, когда оптимизации отключены)

Компиляция с GCC 6 не дает никаких предупреждений вообще.

Теперь мне интересно, код выше определенно имеет типизацию, нет сомнений, но это не так std::aligned_storage предназначен для использования таким образом?

Например, приведенный здесь пример кода обычно не выдает предупреждение с GCC 7, а только потому, что:

  • std::string как-то не влияет,
  • std::aligned_storage доступен со смещением.

Путем изменения std::string в int, удаляя смещение доступа к std::aligned_storage и удалив ненужные детали вы получите это:

#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';
}

И это выдает точно такое же предупреждение:

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*/);
                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Итак, мой вопрос - это ошибка или особенность?

2 ответа

Я не могу ответить, существует ли на самом деле потенциал для неопределенного поведения из-за псевдонимов, или предупреждение является необоснованным. Я считаю тему алиасинга довольно сложным минным полем.

Тем не менее, я думаю, что следующий вариант вашего кода устраняет проблему псевдонимов без каких-либо накладных расходов (и, возможно, более читабельным).

#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';
}

Приемлемо ли это для вашей ситуации, я не уверен.

Ваш код вызывает неопределенное поведение (хотя текст предупреждения является немного касательным к основной причине). В C++ понятия хранения и объекты - это разные вещи. Объекты занимают складские помещения; но хранилище может существовать без объектов в нем.

aligned_storage Механизм обеспечивает хранение без объектов в нем. Вы можете создавать объекты в нем с помощью размещения-нового. Однако ваш код использует оператор присваивания в хранилище, которое не содержит никаких объектов. Если вы обратитесь к определению оператора присваивания, то обнаружите, что в нем нет условий для создания объекта; и фактически он определяет только то, что происходит, когда левая часть обозначает объект, который уже существует.

Код в вашем main должно быть:

new(&storage) int(42);

Обратите внимание, что, поскольку здесь мы работаем с примитивным типом, не требуется выполнять какие-либо вызовы деструктора, и вы можете вызывать Placement New несколько раз в одном и том же пространстве без проблем.

В разделе [basic.life] стандарта рассказывается о том, что вы можете сделать с хранилищем, которое не содержит объектов, и что произойдет, если вы используете новые вызовы размещения или деструкторы для объектов, которые существуют в хранилище.

Смотрите также этот ответ.


Код в cppreference align_storage правильный. Вы предоставляете некоторый неправильный код, основанный на том, что вы описали как "удаление ненужных частей", однако вы удалили очень важную часть, которая была вызовом Placement-New для создания объектов в хранилище:

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

Тогда правильно написать return *reinterpret_cast<const T*>(data+pos); когда pos является допустимым индексом, и выражение обращается к объекту, созданному более ранним вызовом размещения-нового.

Другие вопросы по тегам