[英]Is constructing objects in a char array well-formed
这几乎是标准教科书使用的新位置
template<size_t Len, size_t Align>
class aligned_memory
{
public:
aligned_memory() : data((char*)(((std::uintptr_t)mem + Align - 1) & -Align)) {}
char* get() const {return data;}
private:
char mem[Len + Align - 1];
char* data;
};
template<typename T, size_t N>
class Array
{
public:
Array() : sz(0) {}
void push_back(const T& t)
{
new (data.get() + sz++ * sizeof(T)) T(t);
}
void pop_back()
{
((T*)data.get() + --sz)->~T();
}
private:
aligned_memory<N * sizeof(T), alignof(T)> data;
size_t sz;
};
看起来很好,直到我们看到严格别名,似乎在这是否良好的形式存在一些冲突
他们都同意char*
可能总是引用另一个对象,但有些人指出其形象不对,反之亦然。
很明显,我们的char[]
转换为char*
然后转换为T*
,用它来调用它的析构函数。
那么,上述程序是否违反了严格别名规则? 具体而言,标准中的哪个部分表示格式良好或格式错误?
编辑:作为背景信息,这是在alignas
和std::launder
出现之前为C ++ 0x编写的。 没有特别要求C ++ 0x解决方案,但它是首选。
alignof
是作弊,但它在这里是为了举例。
在无数有用的评论中收集暗示,这是我对正在发生的事情的解释。
TLDR结构
良好
‡见编辑
从[basic.life] †我发现更合乎逻辑的顺序引用†
本国际标准中归属于对象和参考的属性仅在其生命周期内适用于给定对象或参考。
如果一个对象属于类或聚合类型,并且它或其子对象之一由除了普通默认构造函数之外的构造函数初始化,则称该对象具有非空的初始化。 [...]类型
T
对象的生命周期始于:
获得具有适当对齐和
T
型尺寸的存储,并且如果对象具有非空的初始化,则其初始化完成。
类型
T
的对象o的生命周期结束时:
如果T是具有非平凡析构函数的类类型,则析构函数调用将启动,或者
对象占用的存储被释放,或被未嵌套在
o
的对象重用
从[basic.lval] †
如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义
对象的动态类型,
一个cv限定版本的动态类型的对象,
类似于对象的动态类型的类型,
与对象的动态类型对应的有符号或无符号类型的类型,
一种类型,是有符号或无符号类型,对应于对象动态类型的cv限定版本,
聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),
一个类型,它是对象动态类型的(可能是cv限定的)基类类型,
char
,unsigned char
或std::byte
类型。
我们推断出这一点
当另一个对象重用该空间时, char[]
char
的生命周期结束。
调用push_back
时, T
类型对象的生命周期开始。
由于地址((T*)data.get() + --sz)
始终是具有类型T
的对象的生命周期已经开始但尚未结束,因此使用它调用~T()
是有效的。
在此过程中, aligned_memory
的char[]
和char*
别名为T
类型的对象,但这样做是合法的。 此外,没有从它们获得glvalue,因此它们可能是任何类型的指针。
在评论中回答我自己的问题是否使用任何内存作为存储也是格式良好的
U u;
u->~U();
new (&u) T;
((T*)&u)->~T();
new (&u) U;
在上面4点之后,答案是
肯定的
‡见编辑 ,只要U
的对齐不弱于T
‡编辑:我忽略了[basic.life]的另一段
如果在对象的生命周期结束之后并且在重用或释放对象占用的存储之前,则在原始对象占用的存储位置创建新对象,指向原始对象的指针,引用引用原始对象,或者原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,就可以用来操纵新对象,如果:
新对象的存储完全覆盖原始对象占用的存储位置,以及
新对象与原始对象的类型相同(忽略顶级cv限定符),和
原始对象的类型不是const限定的,如果是类类型,则不包含任何类型为const限定的非静态数据成员或引用类型,以及
原始对象是类型为
T
派生程度最高的对象,新对象是类型为T
派生程度最高的对象(也就是说,它们不是基类子对象)。
这意味着即使使用对象格式良好,获得对象的手段也不是。 具体来说,在C ++ 17之后,必须调用std::launder
(std::launder((T*)data.get()) + --sz)->~T();
在C ++ 17之前,解决方法是使用从placement new获取的指针
T* p = new (data.get() + sz++ * sizeof(T)) T(t); // store p somewhere
†从n4659引用,据我所知,同样适用于n1905
Placement-new在指定位置创建一个对象(C ++ 14 expr.new/1),并结束占用该位置的任何其他对象的生命周期(basic.life/1.4)。
代码((T*)data.get() + --sz)->~T();
在存在类型为T
的对象的位置访问类型为T
的对象。 这可以。 如果在该位置曾经有一个char数组则无关紧要。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.