C++标准是否允许使用对象在析构之后但在解除分配之前提供的存储?
很明显,在销毁一个常规对象后,存储空间可以被重用。但是在下面的例子中,对象B
为定义为模板参数T
的对象提供存储空间,并在自己的构造过程中在提供的存储空间中构造该对象。
此外,当B
对象被销毁时,所提供的存储中的对象不会被销毁,并且用于提供存储的数组的生命周期不会结束,因为它有一个无效的平凡析构函数,这导致了A
类型的对象的生命周期是否以B
结束的问题。
#include <cstddef>
#include <iostream>
#include <memory>
#include <new>
struct A
{
int i = 23;
};
template<class T>
struct B
{
B()
{
// construct object T in the provided storage
new (&c) T;
}
// return a reference to the object in the provided storage
T & t()
{
return *std::launder(reinterpret_cast<T *>(&c));
}
// reserved storage
alignas(T) std::byte c[sizeof(T)];
};
int
main()
{
using Alloc = std::allocator<B<A>>;
using AllocTraits = std::allocator_traits<Alloc>;
Alloc alloc;
// storage duration starts
B<A> * b = AllocTraits::allocate(alloc, 1u);
// construction of both B and A
AllocTraits::construct(alloc, b);
// Get a reference to A
A & a = b->t();
std::cout << "At this point A is alive. a.i = " << a.i << std::endl;
a.i = 42;
// object of type B is destroyed, but A is not
AllocTraits::destroy(alloc, b);
// Is it undefined behaviour to read from 'a'?
std::cout << "Is A still alive? a.i = " << a.i << std::endl;
// A is destroyed
a.~A();
// storage duration ends
AllocTraits::deallocate(alloc, b, 1u);
return 0;
}
问:为什么会有人这么做?
答:它允许为弱指针实现一个侵入式控制块,而不会像https://github.com/john-plate/pntr中那样产生额外的开销。
3条答案
按热度按时间dgtucam11#
一旦运行析构函数(
destroy
就是这么做的),b
所指向的位置就不再有对象了,只有存储空间。此时,
a
是一个悬空引用,使用它就是UB。对存储做一些事情并不是UB(例如,您可以使用placement new或
allocator_traits::construct
在那里创建一个新对象)。gudnpqoy2#
由于owner的析构函数在owned子对象之前被调用,因此可能存在UB地雷。
B::B
中的指令序列必须被正确地否定。std::allocator::destroy
可能只调用B::~B
,但不能保证它不会写一些东西作为重新分配的准备。从而覆盖未销毁的A
示例。如果在将来的版本中使用不同的分配器,销毁后的行为就会改变。我猜你最终会得到一个定时炸弹。事实上,在加密上下文中,比如SSL,我会定制
destroy
方法,使用优化栅栏来强制加密器加密已删除的对象。gwbalxhn3#
根据https://en.cppreference.com/w/cpp/language/lifetime
对象的生存期等于或嵌套在其存储的生存期内,请参见存储持续时间。
我认为当你销毁B的时候,c也被销毁了,它的寿命也就结束了。因为c是a的存储器,所以a的寿命也就结束了。
因此,在b被销毁后使用a确实应该是未定义的行为。