在C++中,参考标准,使用放置
new
来获取指向没有成员的结构体的指针,使用另一个没有成员的结构体作为存储,是否安全?类似于以下内容:
struct S1 {};
struct S2 {};
S1 s1;
S2* s2 = new (&s1) S2;
我期望它起作用的直观原因是你“不能用指针做任何事情”——结构体中没有可以访问的成员,所以没有办法使用它(据我所知)访问您不应该访问的内存。但我对标准对此的说明以及引用感兴趣。
假设这没问题,那么对于任何类型
X
和 Y
也可以吗,这样 std::is_empty<X>
和 std::is_empty<Y>
都为 true,假设您没有忘记进行显式析构函数调用?
我对这个问题感兴趣的原因是我有一个这样的类,它打包了延迟构造另一个对象所需的逻辑:
// A container for an object of type T that can be constructed/destroyed at
// will. This is like std::optional but without the overhead of the presence
// bit.
template <typename T>
class ManuallyConstructed {
public:
// Starts uninitialized.
ManuallyConstructed() = default;
// REQUIRES: not initialized.
~ManuallyConstructed() = default;
// Initialize or destroy.
void Init();
void Destroy();
// REQUIRES: currently initialized.
T& operator*();
private:
// Storage for the object, initialized with placement new by Init.
alignas(T) char storage_[sizeof(T)];
};
当
T
根据std::is_empty
为空时,我希望ManuallyConstructed<T>
也为空。这使得它可以在任何时候 [[no_unique_address]]
透明地受益于 T
。但这意味着我需要为存储成员使用空类型,并且我仍然需要能够从 T&
的实现中提供 operator*
。
我认为这是标准允许的,只要大小和对齐方式匹配。 [basic.life]/1 说:
类型的对象的生命周期开始于:T
- 获得具有适合类型
的正确对齐和尺寸的存储,并且T
- 其初始化(如果有)已完成(包括空初始化)([dcl.init]),
[...]
类型为
T
的对象o的生命周期结束于:
- [...]
- 对象占用的存储被释放,或者被未嵌套在o中的对象重用([intro.object])。
这明确承认存储可以重用于另一个对象。 [expr.new] 中没有任何内容似乎表明存储不能来自其他地方。通过使用仅返回用于存储的输入指针的分配函数来放置新的works。
[basic.life]/9 更清楚地表明我们甚至在讨论不通过继承相关的类型,并提供了一个示例(UB,但直到块末尾):
如果程序以静态、线程或自动存储持续时间结束
类型的对象的生命周期,并且T
具有非平凡的析构函数,并且原始类型的另一个对象不占用相同的存储位置当隐式析构函数调用发生时,程序的行为是未定义的。即使该块因异常退出也是如此。T
示例3:
class T { }; struct B { ~B(); }; void h() { B b; new (&b) T; } // undefined behavior at block exit
此外,[basic.lval]/11中的严格别名规则并未被违反,因为要违反它,您必须进行访问,并且访问被定义涉及读取或修改目的。定义明确指出只能访问标量,而这里没有标量。这是原始问题中“无法用它做任何事情”背后的直觉。
也就是说,我相信严格的别名规则即使对于非空类型也不适用,因为原始对象的生命周期已经结束了;我们没有任何意义去访问它。事实上,我想我已经说服自己,即使对于非空类型,这种模式通常也是合法的,只要大小和对齐方式正确。