c ++在家庭制作的矢量容器中放置新的

时间:2015-11-24 23:44:06

标签: c++ placement-new

这里有一些问题非常相似,但它们无法帮我理解它。 另外,我提供了完整的示例代码,因此其他人可能更容易理解。

我制作了一个矢量容器(由于内存原因不能使用stl),过去只使用operator = for push_back *,一旦我来到新的位置,我决定引入一个额外的" emplace_back"它**。

*(T :: operator =应该处理内存管理)

**(该名称取自我后来遇到的std :: vector中的类似函数,我给它的原始名称是一团糟。)

我读了一些关于使用new new over operator new []的危险的东西,但是无法弄清楚以下是否正常,如果没有,它有什么问题,以及是什么我应该替换它,所以我很感激你的帮助。

这是一个简化的代码,没有迭代器,也没有扩展功能,但它说明了这一点:

template <class T>
class myVector {
public :
    myVector(int capacity_) {
        _capacity = capacity_;
        _data = new T[_capacity];
        _size = 0;
    }

    ~myVector() {
        delete[] _data;
    }

    bool push_back(T const & t) {
        if (_size >= _capacity) { return false; }
        _data[_size++] = t;
        return true;
    }

    template <class... Args>
    bool emplace_back(Args const & ... args) {
        if (_size >= _capacity) { return false; }
        _data[_size].~T();
        new (&_data[_size++]) T(args...);
        return true;
    }

    T * erase (T * p) {
        //assert(/*p is not aligned*/);
        if (p < begin() || p >= end()) { return end(); }
        if (p == &back()) { --_size; return end(); }
        *p = back();
        --_size;
        return p;
    }

    // The usual stuff (and more)
    int capacity()          { return _capacity;             }
    int size()              { return _size;                 }
    T * begin()             { return _data;                 }
    T * end()               { return _data + _size;         }
    T const * begin() const { return _data;                 }
    T const * end()   const { return _data + _size;         }
    T & front()             { return *begin();              }
    T & back()              { return *(end() - 1);          }
    T const & front() const { return *begin();              }
    T const & back() const  { return *(end() - 1);          }
    T & operator[] (int i)  { return _data[i];              }
    T const & operator[] (int i) const { return _data[i];   }
private:
    T * _data;
    int _capacity;
    int _size;
};

由于

1 个答案:

答案 0 :(得分:6)

  

我读了一些关于使用新布局的危险的内容   operator new []但是无法确定以下是否正常,   如果没有,它有什么问题[...]

如果您将两个策略混合在一起,那么对于operator new[]与新展示位置相比,它只是非常糟糕(如通常崩溃类型的未定义行为)。

您通常必须做出的主要选择是使用其中一种。如果使用operator new[],则事先为容器的整个容量构造所有元素,并使用push_back等方法覆盖它们。你不会在erase之类的方法中删除它们,只需将它们保留在那里并调整大小,覆盖元素等等。您可以使用operator new[]一次性构造和分配多个元素,并使用operator delete[]一次性销毁和取消分配它们。

为什么Placement New用于标准容器

首先要了解的是,如果要开始滚动自己的向量或其他符合标准的序列(不是简单地将结构与每个节点的一个元素链接在一起),这种方式实际上会在删除元素时将其破坏,在添加时构造元素(不仅仅是覆盖它们),就是分离为容器分配内存并为其构造元素的想法。恰恰相反,在这种情况下,放置新的并不是坏事。实现标准容器的一般品质是必不可少的。但在这种情况下,我们无法将其与operator new[]operator delete[]混合使用。

例如,您可以分配内存以在reserve中保存100个T实例,但您也不想默认构造它们。您希望使用push_backinsertresizefill ctorrange ctorcopy ctor等方法构建它们。实际上添加了元素而不仅仅是容纳它们的能力。这就是为什么我们需要展示位置 new。

否则我们会失去std::vector的一般性,这会避免构建那些不存在的元素,可以复制push_backs中的构造,而不是简单地用operator=覆盖现有的元素等。

所以让我们从构造函数开始:

_data = new T[_capacity];

...这将调用所有元素的默认构造函数。我们不希望这样(既不是默认的ctor要求也不是这个费用),因为使用placement new的整个要点就是在已分配的内存中构造元素,而这已经构建了所有元素。否则,任何在任何地方使用placement的尝试都会尝试第二次构造一个已构造的元素,并且将是UB。

相反,你想要这样的东西:

_data = static_cast<T*>(malloc(_capacity * sizeof(T)));

这只是给了我们一大块字节。

其次,对于push_back,您正在做:

_data[_size++] = t;

尝试使用赋值运算符,并且在我们之前的修改之后,尝试使用尚未构造的未初始化/无效元素。所以我们想:

new(_data + _size) T(t);
++size;

...这使得它使用了复制构造函数。它使它与push_back实际应该做的事情相匹配:在序列中创建新元素而不是简单地覆盖现有元素。

如果要处理容器中间的删除,即使在基本逻辑级别,擦除方法也需要一些工作。但是从资源管理的角度来看,如果使用placement new,则需要手动为已删除的元素调用析构函数。例如:

if (p == &back()) { --_size; return end(); }

......应该更像:

if (p == &back())
{
    --size;
    (_data + _size)->~T();
    return end(); 
}

你的emplace_back手动调用析构函数但它不应该这样做。 emplace_back应该只添加,而不是删除(和销毁)现有元素。它应该与push_back非常相似,只需调用move ctor。

您的析构函数执行此操作:

~myVector() {
   delete[] _data;
}

但是,当我们采用这种方法时,那就是UB。我们想要更像的东西:

~myVector() {
   for (int j=0; j < _size; ++j)
      (_data + j)->~T();
   free(_data);
}

除了异常安全之外,还有更多内容可以覆盖,这是一种完全不同的蠕虫病毒。

但是这应该让你开始考虑在数据结构中对一些内存分配器(在这种示例性情况下为malloc/free)中正确使用placement new。

最后但并非最不重要:

  

(由于记忆原因,不能使用stl)

......这可能是一个不寻常的原因。您的实施不一定使用的内存少于预先调用vector reserve的内存,以便为其提供适当的capacity。您可以选择32位积分并且不需要存储分配器,从而可以在每个容器级别(不是在每个元素级别)上削减几个字节,但它将是一个节省的内存非常少,以换取大量的工作。

这种事情可以是一种有用的学习练习,虽然可以帮助你以更符合标准的方式在标准之外构建一些数据结构(例如:我发现非常有用的展开列表)。

出于ABI原因,我最终不得不重新发明一些vectors和类似矢量的容器(我们想要一个容器,我们可以通过我们的API保证具有相同的ABI,无论使用什么编译器构建一个插件)。即使这样,我也会更喜欢使用std::vector

请注意,如果您只想控制vector如何分配内存,可以通过使用兼容接口指定自己的分配器来实现。这可能很有用,例如,如果您希望vector分配128位对齐的内存,以便与使用SIMD的对齐移动指令一起使用。