如何在异常安全的方式中使用placement new?

时间:2018-04-22 13:27:52

标签: 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 (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();
    }
}

这里有一个隐藏的错误。在T中构造MyStack::Push()可能会导致堆栈缓冲区处于未定义状态(分配的空间将包含垃圾)。稍后,当调用ClearStack时,它将尝试将垃圾重新解释为T并调用其析构函数,这可能会导致访问冲突。

有没有办法通过修改MyStack::Push()来修复此错误? (限制是因为这是一个外部代码,我们更喜欢进行最小的更改,因此更新库相对容易)

我考虑过将MyStack::Push更改为:

T* Push() {
    auto prev = _top;
    T t();
    Reserve(sizeof(T)); 
    _top += sizeof(T);
    reinterpret_cast<T*>(prev) = std::move(t);
    return prev;
}

但它看起来很糟糕,我甚至不确定它不会调用任何UB(并强制T有一个移动构造函数)

这里是否有更好的解决方案来防止抛出构造函数? (最好是MyStack::Push())内的小变化

4 个答案:

答案 0 :(得分:3)

这里的问题实际上是你的设计错了。你正在创建一个类似于std::vector的类型,但它没有“容量”的实际概念。因此,当它Reserve内存时,它真正期望在此过程完成后_top将指向已分配存储的末尾。因此,如果没有,则类型处于无效状态。

这意味着,如果发生异常,您必须撤消Reserve的调用:重新分配旧的存储大小并将该存储中的内容移回 1 。更像vector的实现有3个指针:指向开始的指针,指向第一个未使用的内存字节的指针,以及指向已分配存储结束的指针。这样,如果您Reserve但是获得例外,那么您只需要一些额外的存储空间。

1 :仅供参考:你似乎最想做的事情是行不通的。或者至少,不是大多数用户定义的C ++类型。您的Reserve调用分配新存储并在其中执行memcpy并且从不在这些对象上调用析构函数(因为您不知道它们是什么类型)的可能性很大。嗯,这对于memcpy是有效行为的对象来说是合法的。即,TriviallyCopyable类型。然而,你的Push函数无法防范非TriviallyCopyable类型。

更不用说如果有人有指向旧对象的指针,那么每个Push调用都会使该指针无效。因为你不记得任何物体的类型,所以没有办法重建它们。

答案 1 :(得分:1)

这段代码怎么样:

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;
    }
}

答案 2 :(得分:0)

您可以使用三指针实现:

  • begin指向第一个元素。
  • end指向最后一个元素之后的一个
  • reserved指向保留空间之后的一个元素。

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

  • begin+1=end=reserved表示带有一个元素的已填充容器。
  • begin+1=end;begin+4=reserved表示具有一个元素的容器,另外还有2个空格。

然后你的Push方法看起来像:

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);
}

答案 3 :(得分:-1)

如果您需要堆栈实施,可以尝试使用std::stack

如果你想自己实现它,那么考虑让整个类模板化 - 这将消除对reinterpreter_cast的需要。