“放置新”有什么用途?

问题描述 投票:370回答:22

有没有人曾经使用过C ++的“贴牌新品”?如果是这样,那该怎么办?在我看来,它只对内存映射硬件有用。

c++ memory-management new-operator placement-new
22个回答
337
投票

Placement new允许您在已经分配的内存中构造一个对象。

当您需要构造对象的多个实例时,您可能希望这样做以进行优化,并且每次需要新实例时不再重新分配内存的速度更快。相反,对于可以容纳多个对象的大块内存执行单个分配可能更有效,即使您不想一次性使用所有对象。

DevX给出了good example

标准C ++还支持placement new运算符,它在预分配的缓冲区上构造对象。这在构建内存池,垃圾收集器时或者仅在性能和异常安全性至关重要时非常有用(因为内存已经分配,​​所以没有分配失败的危险,并且在预分配的缓冲区上构建对象需要的时间更少) :

char *buf  = new char[sizeof(string)]; // pre-allocated buffer
string *p = new (buf) string("hi");    // placement new
string *q = new string("hi");          // ordinary heap allocation

您可能还希望确保在关键代码的某个部分(例如,由起搏器执行的代码)中没有分配失败。在这种情况下,您需要先分配内存,然后在临界区中使用placement new。

放置新的释放

您不应该释放使用内存缓冲区的每个对象。相反,您应该只删除[]原始缓冲区。然后,您必须手动调用类的析构函数。有关这方面的好建议,请参阅Stroustrup的常见问题解答:Is there a "placement delete"


8
投票

在序列化时(例如使用boost :: serialization),Placement new也非常有用。在10年的c ++中,这只是我需要新的第二个案例(如果包括访谈,则为第三个))。


8
投票

我认为没有任何答案突出显示这一点,但新位置的另一个好例子和用法是减少内存碎片(通过使用内存池)。这在嵌入式和高可用性系统中特别有用。在最后一种情况下,它特别重要,因为对于必须运行24/365天的系统而言,没有碎片是非常重要的。这个问题与内存泄漏无关。

即使使用了非​​常好的malloc实现(或类似的内存管理功能),也很难长时间处理碎片。在某些时候,如果你不巧妙地管理内存预留/释放呼叫,你可能会遇到很多难以重用的小差距(分配给新的预留)。因此,在这种情况下使用的解决方案之一是使用内存池为应用程序对象预先分配内存。每当你需要某个对象的内存后,你只需使用新的位置在已经保留的内存上创建一个新对象。

这样,一旦您的应用程序启动,您已经保留了所有必需的内存。所有新的内存预留/释放都将转到已分配的池中(您可能有多个池,每个池对应一个不同的对象类)。在这种情况下不会发生内存碎片,因为没有间隙,您的系统可以运行很长时间(数年)而不会受到碎片的影响。

我在实践中专门为VxWorks RTOS看到了这一点,因为它的默认内存分配系统受到很多碎片的影响。因此,在项目中基本上禁止通过标准的new / malloc方法分配内存。所有内存预留应该转到专用内存池。


8
投票

实际上需要实现任何类型的数据结构,该数据结构分配的内存多于插入的元素数量所需的最小内存(即,除了一次分配一个节点的链接结构之外的任何内容)。

unordered_mapvectordeque等容器。这些都分配了比您目前插入的元素所需的最少内存,以避免每次插入都需要堆分配。让我们用vector作为最简单的例子。

当你这样做时:

vector<Foo> vec;

// Allocate memory for a thousand Foos:
vec.reserve(1000);

......实际上并没有构建一千个Foos。它只是为它们分配/保留内存。如果vector没有在这里使用placement new,它将默认构建Foos到处以及必须调用它们的析构函数,即使对于你从未插入过的元素也是如此。

分配!=建筑,释放!=毁灭

一般来说,要实现如上所述的许多数据结构,你不能将分配内存和构造元素视为一个不可分割的东西,同样你也不能将释放内存和破坏元素视为一个不可分割的东西。

这些想法之间必须存在分离,以避免不必要地左右调用构造函数和析构函数,这就是为什么标准库将std::allocator(在分配/释放内存*时不构造或销毁元素)的思想分开的原因从使用它的容器中,使用placement new手动构造元素,并使用析构函数的显式调用手动销毁元素。

  • 我讨厌std::allocator的设计,但这是一个不同的主题,我会避免咆哮。 :-D

所以无论如何,我倾向于使用它很多,因为我编写了许多通用的符合标准的C ++容器,这些容器无法根据现有容器构建。其中包括我几十年前建立的一个小矢量实现,以避免常见情况下的堆分配,以及一个内存高效的trie(一次不分配一个节点)。在这两种情况下,我都无法使用现有的容器实现它们,所以我不得不使用placement new来避免在左右不必要的事情上多次调用构造函数和析构函数。

当然,如果您使用自定义分配器来单独分配对象(如空闲列表),那么您通常也希望使用placement new,这样(基本示例不涉及异常安全或RAII):

Foo* foo = new(free_list.allocate()) Foo(...);
...
foo->~Foo();
free_list.free(foo);

7
投票

我用它来存储具有内存映射文件的对象。 具体的例子是一个图像数据库,它处理大量的大图像(超过可能适合内存)。


7
投票

我已经看到它用作slight performance hack for a "dynamic type" pointer(在“引擎盖下”部分):

但是这里有一个棘手的技巧我用来获得小类型的快速性能:如果保持的值可以放在void *中,我实际上并不打算分配一个新对象,我使用placement new强制它进入指针本身。


7
投票

它被std::vector<>使用,因为std::vector<>通常比objects中的vector<>分配更多的内存。


6
投票

我用它来创建基于包含从网络接收的消息的内存的对象。


5
投票

通常,使用placement new来摆脱“普通新”的分配成本。

我使用它的另一个场景是我想要访问仍然要构造的对象的指针,以实现每个文档的单例。



4
投票

我遇到的一个地方是容器,它分配一个连续的缓冲区,然后根据需要用对象填充它。如上所述,std :: vector可能会这样做,我知道某些版本的MFC CArray和/或CList就是这样做的(因为那是我第一次遇到它的地方)。缓冲区过度分配方法是一种非常有用的优化,而placement new几乎是在该场景中构造对象的唯一方法。它有时也用于在直接代码之外分配的内存块中构造对象。

我已经以相似的身份使用它,虽然它没有经常出现。不过,它是C ++工具箱的有用工具。


57
投票

我们将它与自定义内存池一起使用。只是一个草图:

class Pool {
public:
    Pool() { /* implementation details irrelevant */ };
    virtual ~Pool() { /* ditto */ };

    virtual void *allocate(size_t);
    virtual void deallocate(void *);

    static Pool::misc_pool() { return misc_pool_p; /* global MiscPool for general use */ }
};

class ClusterPool : public Pool { /* ... */ };
class FastPool : public Pool { /* ... */ };
class MapPool : public Pool { /* ... */ };
class MiscPool : public Pool { /* ... */ };

// elsewhere...

void *pnew_new(size_t size)
{
   return Pool::misc_pool()->allocate(size);
}

void *pnew_new(size_t size, Pool *pool_p)
{
   if (!pool_p) {
      return Pool::misc_pool()->allocate(size);
   }
   else {
      return pool_p->allocate(size);
   }
}

void pnew_delete(void *p)
{
   Pool *hp = Pool::find_pool(p);
   // note: if p == 0, then Pool::find_pool(p) will return 0.
   if (hp) {
      hp->deallocate(p);
   }
}

// elsewhere...

class Obj {
public:
   // misc ctors, dtors, etc.

   // just a sampling of new/del operators
   void *operator new(size_t s)             { return pnew_new(s); }
   void *operator new(size_t s, Pool *hp)   { return pnew_new(s, hp); }
   void operator delete(void *dp)           { pnew_delete(dp); }
   void operator delete(void *dp, Pool*)    { pnew_delete(dp); }

   void *operator new[](size_t s)           { return pnew_new(s); }
   void *operator new[](size_t s, Pool* hp) { return pnew_new(s, hp); }
   void operator delete[](void *dp)         { pnew_delete(dp); }
   void operator delete[](void *dp, Pool*)  { pnew_delete(dp); }
};

// elsewhere...

ClusterPool *cp = new ClusterPool(arg1, arg2, ...);

Obj *new_obj = new (cp) Obj(arg_a, arg_b, ...);

现在,您可以在单个内存区域中将对象聚集在一起,选择一个速度非常快但不进行解除分配的分配器,使用内存映射以及您希望通过选择池并将其作为参数传递给对象的放置而强加的任何其他语义新运营商。


4
投票

脚本引擎可以在本机接口中使用它来从脚本中分配本机对象。有关示例,请参见Angelscript(www.angelcode.com/angelscript)。


3
投票

请参阅http://xll.codeplex.com的xll项目中的fp.h文件。它解决了那些喜欢随身携带它们的数组的数组的“无编辑的编译器问题”问题。

typedef struct _FP
{
    unsigned short int rows;
    unsigned short int columns;
    double array[1];        /* Actually, array[rows][columns] */
} FP;

2
投票

以下是C ++就地构造函数的杀手级用法:与高速缓存行对齐,以及2个边界的其他权限。这是my ultra-fast pointer alignment algorithm to any power of 2 boundaries with 5 or less single-cycle instructions

/* Quickly aligns the given pointer to a power of two boundary IN BYTES.
@return An aligned pointer of typename T.
@brief Algorithm is a 2's compliment trick that works by masking off
the desired number in 2's compliment and adding them to the
pointer.
@param pointer The pointer to align.
@param boundary_byte_count The boundary byte count that must be an even
power of 2.
@warning Function does not check if the boundary is a power of 2! */
template <typename T = char>
inline T* AlignUp(void* pointer, uintptr_t boundary_byte_count) {
  uintptr_t value = reinterpret_cast<uintptr_t>(pointer);
  value += (((~value) + 1) & (boundary_byte_count - 1));
  return reinterpret_cast<T*>(value);
}

struct Foo { Foo () {} };
char buffer[sizeof (Foo) + 64];
Foo* foo = new (AlignUp<Foo> (buffer, 64)) Foo ();

现在不只是在你脸上露出笑容(:-)。我♥♥♥C ++ 1x


50
投票

如果要将分配与初始化分开,这很有用。 STL使用placement new来创建容器元素。


32
投票

我在实时编程中使用它。我们通常不希望在系统启动后执行任何动态分配(或解除分配),因为无法保证需要多长时间。

我能做的是预先分配一大块内存(大到足以容纳类可能需要的任何数量)。然后,一旦我在运行时弄清楚如何构造事物,可以使用placement new来构建我想要的对象。我知道我使用它的一种情况是帮助创建一个异构的circular buffer

这当然不适合胆小的人,但这就是为什么他们为它的语法做得有点粗糙。


23
投票

我用它来构造通过alloca()在堆栈上分配的对象。

无耻的插件:我在博客上发表了关于here的消息。


13
投票

Head Geek:BINGO!你完全得到了它 - 这正是它的完美之处。在许多嵌入式环境中,外部约束和/或整体使用场景迫使程序员将对象的分配与其初始化分开。集中在一起,C ++称之为“实例化”;但是每当必须在没有动态或自动分配的情况下显式调用构造函数的操作时,就可以使用placement new。它也是定位固定到硬件组件地址(内存映射I / O)的全局C ++对象的完美方式,或者是出于任何原因必须驻留在固定地址的任何静态对象。


11
投票

我用它来创建一个Variant类(即一个可以表示单个值的对象,它可以是多种不同类型之一)。

如果Variant类支持的所有值类型都是POD类型(例如int,float,double,bool),那么标记的C样式联合就足够了,但是如果你想要一些值类型是C ++对象(例如std :: string),C union特性不会这样做,因为非POD数据类型可能不会被声明为union的一部分。

因此,我将分配一个足够大的字节数组(例如sizeof(the_largest_data_type_I_support)),并在Variant设置为保存该类型的值时,使用placement new来初始化该区域中的相应C ++对象。 (当然,当切换到不同的非POD数据类型时,预先删除位置)


9
投票

当您想要重新初始化全局或静态分配的结构时,它也很有用。

旧的C方式是使用memset()将所有元素设置为0.由于vtable和自定义对象构造函数,您无法在C ++中执行此操作。

所以我有时会使用以下内容

 static Mystruct m;

 for(...)  {
     // re-initialize the structure. Note the use of placement new
     // and the extra parenthesis after Mystruct to force initialization.
     new (&m) Mystruct();

     // do-some work that modifies m's content.
 }

8
投票

如果您正在构建内核,那么它​​是非常有用的 - 您在哪里放置从磁盘或页面表中读取的内核代码?你需要知道去哪里。

或者在其他非常罕见的情况下,例如当你有足够的分配空间并希望将一些结构放在彼此之后。它们可以通过这种方式打包,而无需offsetof()运算符。不过,还有其他一些技巧。

我也相信一些STL实现使用了新的贴图,比如std :: vector。它们为这两个元素分配空间,并且不需要总是重新分配。

© www.soinside.com 2019 - 2024. All rights reserved.