Код, который я написал, был без предупреждений в 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*/);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Итак, мой вопрос — это ошибка или особенность?
Я не могу ответить, существует ли на самом деле потенциал для неопределенного поведения из-за псевдонимов, или предупреждение является необоснованным. Я считаю тему алиасинга довольно сложным минным полем.
Тем не менее, я думаю, что следующий вариант вашего кода устраняет проблему псевдонимов без каких-либо накладных расходов (и, возможно, является более читабельным).
#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
является допустимым индексом, и выражение обращается к объекту, созданному более ранним вызовом размещения-нового.