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

Код, который я написал, был без предупреждений в 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*/);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

4

Решение

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

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

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

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

3

Другие решения

Ваш код вызывает неопределенное поведение (хотя текст предупреждения является немного касательным к основной причине). В 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 является допустимым индексом, и выражение обращается к объекту, созданному более ранним вызовом размещения-нового.

1

По вопросам рекламы [email protected]