c++在自制vector容器中的新位置

c++ placement new in a home made vector container

本文关键字:新位置 位置 vector c++      更新时间:2023-10-16

这里有一些非常相似的问题,但它们无法帮助我理清思路。另外,我给出了一个完整的示例代码,所以它可能更容易为其他人理解。

我做了一个vector容器(由于内存原因不能使用stl),它过去只对push_back*使用operator=,一旦遇到放置new,我决定向它引入一个额外的"emplace_back"**。

*(T::operator=期望处理内存管理)

**(名称取自std::vector中的一个类似函数,我稍后会遇到,我给它的原始名称很混乱)。

我读了一些关于使用placement new而不是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;
};

感谢

我读了一些关于使用位置new over的危险的文章Operator new[],但无法判断以下操作是否正确,如果没有,有什么问题[…]

对于operator new[]与植入new,只有将这两种策略混合在一起才会真正糟糕(如典型的未定义行为崩溃类型)。

您通常必须做出的主要选择是使用其中一个或另一个。如果使用operator new[],则提前为容器的整个容量构造所有元素,并在push_back等方法中覆盖它们。你不会在像erase这样的移除方法中破坏它们,只是保留它们并调整大小,覆盖元素,等等。你既可以用operator new[]一次性构造和分配多个元素,也可以用operator delete[]一次性销毁和释放它们。

为什么在标准容器中使用New位置

如果您想开始滚动自己的vector或其他符合标准的序列(不只是每个节点有一个元素的链接结构),那么首先要理解的是,将为容器分配内存和在适当位置为其构造元素的想法分开,这种方式实际上是在删除元素时销毁元素,在添加元素时构造元素(而不仅仅是覆盖它们)。所以恰恰相反,在这种情况下,新位置并不坏。达到标准容器的一般质量是基本的需要。但在这种情况下,我们不能将其与operator new[]operator delete[]混合使用。

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

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

让我们从构造函数开始:

_data = new T[_capacity];

…这将调用所有元素的默认构造函数。我们不希望出现这种情况(既不需要默认的actor要求,也不需要这个开销),因为使用placement new的目的是在分配内存的地方构造元素,而这已经构造了所有的元素。否则,任何位置new的使用都将尝试第二次构造一个已经构造好的元素,并且将是UB。

你应该这样写:

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

这只是给我们一个原始的字节块。

第二,对于push_back,你在做:

_data[_size++] = t;

这是尝试使用赋值操作符,并且,在我们之前的修改之后,在一个未初始化/无效的尚未构造的元素上。所以我们想要:

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

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

如果你想处理容器中间的删除操作,你的erase方法甚至需要在基本逻辑层面做一些工作。但是从资源管理的角度来看,如果使用placement new,则需要手动调用已删除元素的析构函数。例如:

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

…应该更像:

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

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

你的析构函数这样做:

~myVector() {
delete[] _data;
}

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

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

还有很多东西要覆盖,比如异常安全,这是一个完全不同的问题。

但是这应该让您开始正确使用数据结构中针对某些内存分配器(在这个示例中是malloc/free)的位置new。

最后但并非最不重要的:

(由于内存原因不能使用stl)

…这可能是一个不寻常的原因。您的实现并不一定使用比vector更少的内存,提前调用reserve来为其提供适当的capacity。在每个容器级别(而不是每个元素级别)选择32位整数时,您可能会节省一些字节,并且不需要存储分配器,但这将是非常小的内存节省,以换取大量的工作。

这类东西可以是一个有用的学习练习,尽管它可以帮助你以更符合标准的方式在标准之外构建一些数据结构(例如:展开列表,我发现它非常有用)。

由于ABI的原因,我最终不得不重新发明一些vectors和类矢量容器(我们想要一个容器,我们可以通过我们的API,无论使用什么编译器来构建插件,都保证具有相同的ABI)。即便如此,我还是更喜欢简单地使用std::vector

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