这里有一些问题非常相似,但它们无法帮我理解它。 另外,我提供了完整的示例代码,因此其他人可能更容易理解。
我制作了一个矢量容器(由于内存原因不能使用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;
};
由于
答案 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_back
,insert
,resize
,fill ctor
,range ctor
,copy 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的对齐移动指令一起使用。