C++ 如何以异常安全的方式使用placement new?

C++ 如何以异常安全的方式使用placement new?,c++,new-operator,C++,New Operator,假设我有一个MyStack类,它公开了: class MyStack { public: template <typename T> T* Push() { Reserve(sizeof(T)); // Make sure that the buffer can hold an additional sizeof(T) bytes , realloc if needed auto prev= _top; _top += sizeof(T); new

假设我有一个
MyStack
类,它公开了:

class MyStack {
public:

template <typename T>
T* Push() {
    Reserve(sizeof(T)); // Make sure that the buffer can hold an additional sizeof(T) bytes , realloc if needed
    auto prev= _top;
    _top += sizeof(T);
    new (prev) T();
    return reinterpret_cast<T*>(prev);
}

template <typename T>
T* Pop() {
    _top -= sizeof(T);
    return return reinterpret_cast<T*>(_top);
}

bool Empty() const {
    return _bottom == _top;
}

private:
    char* _bottom;
    char* _top;
};

// Assumes all stack elements have the same type
template <typename T>
void ClearStack(MyStack& stack) {
    while (!stack.Empty()) {
        stack.template Pop<T>()->~T();
    }
}
但是它看起来很糟糕,我甚至不确定它是否调用了任何UB(并且还强制
t
使用move构造函数)


这里有更好的解决方案来防止抛出构造函数吗?(最好是在MyStack::Push()内部进行一个小的更改)

这里的问题实际上是您的设计是错误的。您正在创建的类型的行为有点像
std::vector
,但它没有实际的“容量”概念。因此,当它
保留
内存时,它真诚地希望
\u top
在该过程完成后将指向分配的存储的末尾。因此,如果没有,则类型处于无效状态

这意味着,在发生异常的情况下,您必须撤消对
Reserve
:重新分配旧存储大小并将该存储中的内容移回1的调用。更类似于向量的实现有3个指针:一个指向开始的指针,一个指向内存中第一个未使用字节的指针,以及一个指向已分配存储的结束的指针。这样,如果您
保留了
,但得到了一个异常,那么您只是得到了一些额外的存储空间

1:仅供参考:你似乎想做的事情很可能不会奏效。或者至少不是大多数用户定义的C++类型。很可能您的
Reserve
调用会分配新存储,并对其执行
memcpy
操作,并且从不对这些对象调用析构函数(因为您不知道它们是什么类型)。嗯,这只适用于
memcpy
是有效操作的对象。也就是说,平凡的可复制类型。然而,您的
Push
函数没有任何东西可以防止非平凡的可复制类型


更不用说,如果任何人有一个指向旧对象的指针,那么每次
Push
调用都会使该指针无效。而且,由于您不记得任何对象的类型,因此无法重新组合它们。

这里的问题实际上是您的设计是错误的。您正在创建的类型的行为有点像
std::vector
,但它没有实际的“容量”概念。因此,当它
保留
内存时,它真诚地希望
\u top
在该过程完成后将指向分配的存储的末尾。因此,如果没有,则类型处于无效状态

这意味着,在发生异常的情况下,您必须撤消对
Reserve
:重新分配旧存储大小并将该存储中的内容移回1的调用。更类似于向量的实现有3个指针:一个指向开始的指针,一个指向内存中第一个未使用字节的指针,以及一个指向已分配存储的结束的指针。这样,如果您
保留了
,但得到了一个异常,那么您只是得到了一些额外的存储空间

1:仅供参考:你似乎想做的事情很可能不会奏效。或者至少不是大多数用户定义的C++类型。很可能您的
Reserve
调用会分配新存储,并对其执行
memcpy
操作,并且从不对这些对象调用析构函数(因为您不知道它们是什么类型)。嗯,这只适用于
memcpy
是有效操作的对象。也就是说,平凡的可复制类型。然而,您的
Push
函数没有任何东西可以防止非平凡的可复制类型

更不用说,如果任何人有一个指向旧对象的指针,那么每次
Push
调用都会使该指针无效。由于您不记得任何对象的类型,因此无法重新组合它们。

这段代码如何:

template <typename T>
T* Push() {
    Reserve(sizeof(T));
    auto prev= _top;
    _top += sizeof(T);
    try {
        new (prev) T();
        return reinterpret_cast<T*>(prev);
    }
    catch (...) {
        Unreserve(sizeof(T)); //release the memory, optional?
        _top = prev;            
        throw;
    }
}
模板
T*Push(){
储量(T);
自动上一个=_顶部;
_top+=尺寸f(T);
试一试{
新(上)T();
返回重新解释(上一个);
}
捕获(…){
Unreserve(sizeof(T));//释放内存,可选吗?
_顶部=上一个;
投掷;
}
}
这个代码怎么样:

template <typename T>
T* Push() {
    Reserve(sizeof(T));
    auto prev= _top;
    _top += sizeof(T);
    try {
        new (prev) T();
        return reinterpret_cast<T*>(prev);
    }
    catch (...) {
        Unreserve(sizeof(T)); //release the memory, optional?
        _top = prev;            
        throw;
    }
}
模板
T*Push(){
储量(T);
自动上一个=_顶部;
_top+=尺寸f(T);
试一试{
新(上)T();
返回重新解释(上一个);
}
捕获(…){
Unreserve(sizeof(T));//释放内存,可选吗?
_顶部=上一个;
投掷;
}
}

您可以使用三指针实现:

  • begin
    指向第一个元素
  • end
    指向最后一个元素后面的一个
  • reserved
    指向超过保留空间的一个元素

  • begin=end=reserved(=nullptr)
    表示未分配的容器

  • begin+1=end=reserved
    表示用一个元素填充的容器
  • begin+1=结束;begin+4=保留
    表示包含一个元素的容器和可容纳两个以上元素的空间
然后,您的推送方法如下所示:

template <typename T>
T* Push() {
    if(end==reserved)
        //relocate, ensure that begin<=end<reserved
    new (end) T();
    end+=sizeof(T);
    return reinterpret_cast<T*>(end-1);
}
模板
T*Push(){
如果(结束==保留)

//重新定位,确保begin可以使用三指针实现:

  • begin
    指向第一个元素
  • end
    指向最后一个元素后面的一个
  • reserved
    指向超过保留空间的一个元素

  • begin=end=reserved(=nullptr)
    表示未分配的容器

  • begin+1=end=reserved
    表示用一个元素填充的容器
  • begin+1=end;begin+4=reserved
    表示包含一个元素和两个以上空间的容器
然后,您的推送方法如下所示:

template <typename T>
T* Push() {
    if(end==reserved)
        //relocate, ensure that begin<=end<reserved
    new (end) T();
    end+=sizeof(T);
    return reinterpret_cast<T*>(end-1);
}
模板
T*Push(){
如果(结束==保留)

//重新定位,确保开始如果需要堆栈实现,可以尝试使用

如果您想自己实现它,那么考虑将整个类模板化——这样就消除了对<代码>的需要。