GCC 7,aligned_storage 和“取消引用类型双关指针将破坏严格别名规则”
Posted
技术标签:
【中文标题】GCC 7,aligned_storage 和“取消引用类型双关指针将破坏严格别名规则”【英文标题】:GCC 7, aligned_storage and "dereferencing type-punned pointer will break strict-aliasing rules" 【发布时间】:2017-04-30 21:35:32 【问题描述】:我编写的代码在 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
不应该这样使用吗?
例如,here 给出的示例代码通常不会在 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*/);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
所以我的问题是 - 这是一个错误还是一个功能?
【问题讨论】:
你不会那样使用aligned_storage
。相反,你会使用int * p = new (&storage) int(42);
。
@KerrekSB - 请注意,来自 cppreference 的示例与我所做的完全相同。
嗯。永远不要相信你自己没有编辑过的维基?
@KerrekSB - 这不是信任问题,我只是想确定这是否是 GCC 的预期行为。 cppreference 做了同样的事情这一事实证明了我不是唯一一个想以这种方式使用它的人(;当然,placement new 会更好,但实际上在我的实际用例中它会使事情变得不必要地复杂化。跨度>
std::aligned_storage 上 cppreference 页面中的示例是新的展示位置。你说的是另一个页面吗?
【参考方案1】:
我无法回答是否真的存在由于别名而导致的未定义行为的可能性,或者警告是否没有根据。我发现别名主题是一个相当复杂的雷区。
但是,我认为您的代码的以下变体消除了别名问题而没有任何开销(并且可能更具可读性)。
#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';
你的情况是否可以接受,我不能确定。
【讨论】:
如果你想过度对齐你的存储,这是没有用的,例如对于 SIMD。例如,将alignof(T)>::type
更改为16
会将static_vector<int, 10> v1;
的大小从48 = 10*4 + sizeof(size_t)
更改为176 = 10*16 + size_t + padding
。所以你没有得到一个对齐的数组,你得到一个对齐/填充元素的数组。如果您只支持使用T
的默认对齐方式,那么我看不到重点,T data[N];
已经这样做了。
哦,我猜std::aligned_storage
对于您想要延迟构造的具有默认构造函数的类型很有用。 For which purposes needs std::aligned_storage?。如果你想要 SIMD 的过度对齐,alignas(32) float foo[N]
仍然很好,但我希望std::aligned_storage
可以让我们可移植地声明一个函数接受一个指向 float
的 16 对齐数组的指针。而不是像 __builtin_assume_aligned
or a GCC-only typedef of aligned float 这样的 GNU 专用的东西【参考方案2】:
您的代码会导致未定义的行为(尽管警告文本与根本原因有点相干)。在 C++ 中,概念 storage 和 objects 是不同的东西。对象占用存储;但是存储可能没有对象存在。
aligned_storage
机制提供没有对象的存储。您可以使用placement-new 在其中创建对象。但是,您的代码在不包含任何对象的存储上使用赋值运算符。如果您查阅赋值运算符的定义,您会发现它没有创建对象的规定;实际上它只定义了当左侧指定一个已经存在的对象时会发生什么。
main
中的代码应该是:
new(&storage) int(42);
请注意,由于我们在这里使用的是原始类型,因此不需要执行任何形式的析构函数调用,并且您可以在同一空间上多次调用placement-new 没有问题。
标准的 [basic.life] 部分讨论了您可以对不包含对象的存储执行什么操作,以及如果您对存储中确实存在的对象使用placement-new 或析构函数调用会发生什么。
另见this answer。
cppreferencealigned_storage 中的代码是正确的。您根据您描述为“删除不相关部分”的内容提供了一些不正确的代码,但是您删除了一个非常相关的部分,即调用placement-new 以在存储中创建对象:
new(data+m_size) T(std::forward<Args>(args)...);
那么当pos
是一个有效的索引时,写return *reinterpret_cast<const T*>(data+pos);
是正确的,并且该表达式访问由先前的placement-new 调用创建的对象。
【讨论】:
我投了反对票,因为即使所有这些在技术上都是正确的,但它并不能真正回答为什么 gcc 会发出警告的问题(除非你在 gcc 7.1 中指出错误行为没有确切地说)。new (&storage) int(42); *reinterpret_cast<int*>(&storage) = 43;
still has the same warning(将鼠标悬停在绿色曲线上)。
@zneak OP 清楚地认为他们的代码是正确的,所以我认为解释为什么代码是未定义的行为是相关的。 UB 并产生警告的 IMO 代码不足以证明该警告是错误。事实上,该警告对于 OP 的代码在技术上是正确的,并且在发现问题方面证明有些用处。
您的评论确实显示了编译器错误,也许可以作为新问题发布。错误修复可能是删除代码的警告,并为 OP 的代码保留警告(可能措辞不同)。
请注意,我在第一条评论中描述的行为确实是 GCC 错误,现在已修复。 gcc.gnu.org/bugzilla/show_bug.cgi?id=80593
您对 cppreference 中的示例的评论是错误的 - 无论其他地方是否有新的展示位置都不会改变 reinterpret_cast 产生警告的事实,而它在早期的 GCC 中没有产生警告版本。以上是关于GCC 7,aligned_storage 和“取消引用类型双关指针将破坏严格别名规则”的主要内容,如果未能解决你的问题,请参考以下文章
如何复制boost :: aligned_storage对象中的数据?
Mac 上的 gcc 4.7.3 和 gdb 7.6 - 奇怪的步进问题