“新安置”有什么用途?


410

这里有没有人使用过C ++的“ placement new”?如果是这样,那是为了什么?在我看来,它仅在内存映射的硬件上有用。


14
这只是我一直在寻找的信息,用于在boost分配的内存池上调用对象构造函数。(希望这些关键字可以使将来有人更容易找到它)。
杂耍节目Bob

2
联合的构造函数中的C ++ 11 Wikipedia文章中使用了它。
HelloGoodbye 2015年

@HelloGoodbye,有趣!在您链接的文章中,为什么您不能只是p = pt使用Point而不是使用赋值运算符new(&p) Point(pt)呢?我不知道两者之间的区别。前者会operator=在Point上调用,而后者会在上调用复制构造函数Point吗?但是我仍然不清楚为什么一个要比另一个更好。
安德烈·尼古拉·彼得

@ Andrei-NiculaePetre我本人还没有使用过new放置,但是如果您当前没有该类的对象,我想您应该将其与复制构造函数一起使用,否则应该使用复制赋值运算符。除非阶级是琐碎的;那么使用哪一个都没关系。销毁对象也同样。未能对非平凡的类正确处理此问题很可能导致奇怪的行为,甚至在某些情况下甚至可能导致未定义的行为
HelloGoodbye

@ Andrei-NiculaePetre实际上,我发现Wikipedia文章中的示例非常糟糕,因为它只是假设不存在先前的对象,因此需要构造一个对象。如果U::operator=刚刚被调用,则不是这种情况。
HelloGoodbye

Answers:


364

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

当您需要构造一个对象的多个实例时,您可能需要这样做以进行优化,并且在每次需要一个新实例时不重新分配内存会更快。取而代之的是,即使您不想一次使用所有内存,对可以容纳多个对象的一块内存执行单个分配可能更有效。

DevX就是一个很好的例子

标准C ++还支持放置新运算符,该运算符可在预分配的缓冲区上构造一个对象。这在构建内存池,垃圾回收器或仅在性能和异常安全至关重要时非常有用(因为已经分配了内存,因此没有分配失败的危险,并且在预分配的缓冲区上构造对象所花费的时间更少) :

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

您可能还想确保关键代码的某些部分(例如,在起搏器执行的代码中)不会出现分配失败。在这种情况下,您可能希望先分配内存,然后在关键部分中使用new放置。

重新分配刊登位置

您不应该释放使用内存缓冲区的每个对象。相反,您应该只删除[]原始缓冲区。然后,您必须手动调用类的析构函数。有关此问题的好的建议,请参阅Stroustrup的FAQ,有关以下内容:是否存在“展示位置删除”


54
不建议您弃用它,因为您需要此功能来有效地实现容器对象(如vector)。如果您不是在构建自己的容器,则无需使用此功能。
马丁·约克

26
记住#include <memory>也是非常重要的,否则您可能会在某些无法自动识别新位置的平台上遇到一些令人头疼的麻烦
Ramon Zarazua B. 09/09/24

22
严格来说,调用delete[]原始char缓冲区是未定义的行为。使用放置newchar通过重新使用原始对象的存储空间来结束它们的生命周期。如果现在调用delete[] buf所指向的对象的动态类型不再与它们的静态类型匹配,那么您将具有不确定的行为。使用operator new/ operator delete分配原先要由放置使用的原始内存更为一致new
CB Bailey 2010年

31
我肯定会跳过在起搏器上使用堆的方法:-)
Eli Bendersky

15
@RamonZarazua错误的标题,是#include <new>
bit2shift '16

63

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

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, ...);

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


1
是的 我们对此非常聪明,但是对于这个问题来说却是题外话。
唐·韦克菲尔德

2
@jdkoftinoff您是否有指向实际代码示例的链接?对我来说似乎很有趣!
维克多

@DonWakefield您如何处理该池中的对齐方式?您不应该将对齐方式作为参数传递给allocate()某个地方吗?
米哈伊尔·瓦西里耶夫

1
@MikhailVasilyev,在实际的实现中,您当然可以解决这个问题。仅示例代码。
唐·韦克菲尔德

如果展示位置是无效地址,例如0x0,该怎么办?
查理

51

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


35

我已经在实时编程中使用了它。系统启动后,我们通常希望执行任何动态分配(或取消分配),因为无法保证这将花费多长时间。

我可以做的是预分配很大的内存(足够大,可以容纳该类可能需要的任何数量的内存)。然后,一旦我在运行时弄清楚了如何构造事物,就可以将placement new用于在我想要的地方构造它们。我知道我用过的一种情况是帮助创建一个异构的循环缓冲区

当然,这并不是出于胆小,这就是为什么他们使它的语法有些粗糙。


TED,您好,请您分享更多有关您所拥有的解决方案的信息。我正在考虑一个预先分配的解决方案,但进展不大。先感谢您!
越南

1
好吧,实际的大量循环缓冲区代码确实是正确的棘手部分。新的地方看起来有点令人不快,但相比之下,这根本没有问题。
TED

26

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

shameless plug:我在这里博客了。


有趣的文章,但我不确定我是否可以理解使用over的好处boost::array。您可以对此进行扩展吗?
GrahamS 2011年

boost :: array要求数组的大小是编译时常量。这没有那个限制。
Ferruccio

2
@Ferruccio这很酷,我确实注意到您的宏虽然有点不安全,即大小可能是一个表达式。例如,如果传入x + 1,则将其扩展为sizeof(type)* x + 1,这是不正确的。您需要将宏括起来以使其更安全。
Benj 2012年

如果引发异常,则与alloca一起使用对我来说很危险,因为您必须在所有对象上调用析构函数。
CashCow

14

怪胎头:宾果!您完全了解了-这正是它的完美之选。在许多嵌入式环境中,外部约束和/或总体使用情况迫使程序员将对象的分配与其初始化分开。C ++一起称为“实例化”。但是无论何时必须在没有动态分配或自动分配的情况下显式调用构造函数的操作时,都可以使用new放置方法。这也是查找固定在硬件组件(内存映射的I / O)或任何静态对象(无论出于何种原因必须驻留在固定地址)上的全局C ++对象的理想方法。


12

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

如果Variant类支持的所有值类型都是POD类型(例如int,float,double,bool),则标记的C样式联合就足够了,但是如果您希望某些值类型成为C ++对象(例如std :: string),C联合功能将不起作用,因为非POD数据类型可能不会声明为联合的一部分。

因此,我分配了一个足够大的字节数组(例如sizeof(the_largest_data_type_I_support)),并在Variant设置为保留该类型的值时,使用new放置初始化该区域中的相应C ++对象。(当然,当切换到其他非POD数据类型时,布局会事先删除)


可以在联合中声明Erm,非POD数据类型,只要您提供联合ctor(嘿),那么new该ctor 可能会使用放置来初始化其非POD子类。参考:stackoverflow.com/a/33289972/2757035使用任意大字节数组重塑此轮子是一件令人印象深刻的杂技,但似乎完全没有必要,那么,我错过了什么?:)
underscore_d 2015年

6
您错过了C ++ 11之前的所有C ++版本,在许多情况下仍然需要支持。:)
杰里米·弗里斯纳

10

序列化时(例如使用boost :: serialization),new放置也非常有用。在c ++的10年中,这只是我需要重新布置的第二种情况(如果包括面试,则是第三种情况:))。


9

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

旧的C方法memset()用于将所有元素设置为0。由于vtables和自定义对象构造函数的存在,您无法在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.
 }

1
这样重新初始化之前,是否不需要进行相应的销毁?
极客头球

[拼写编辑]通常-您需要。但是有时候,当您知道该类未分配内存或其他资源时(或者您在外部释放了它们-例如,当您使用内存池时),可以使用此技术。它确实确保v表指针不会被覆盖。– nimrodm 16小时前
nimrodm

1
即使在C语言中,也只能保证将所有位设置为0才能为整数类型(而不是其他类型)生成0的表示(空指针可以具有非零的表示)。
curiousguy

@curiousguy-对于原始类型,您是正确的(它将使程序可预测,这在调试时是一个优点)。但是,C ++数据类型将使它们的构造函数运行(就地)并被正确初始化。
nimrodm 2015年

9

实际上,实现某种类型的数据结构所分配的内存要比插入的元素数量的最低要求要多(即,除了一个链接结构同时分配一个节点之外的其他任何事物),实际上是一种要求。

取容器,如unordered_mapvectordeque。这些都分配了比到目前为止您已插入的元素所需的最低要求更多的内存,以避免需要为每个插入操作分配堆。让我们vector作为最简单的示例。

当您这样做时:

vector<Foo> vec;

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

...实际上并不能构成一千个Foos。它只是为它们分配/保留内存。如果vector此处未使用new放置,则将Foos在整个位置进行默认构造,甚至对于从未插入的元素都必须调用其析构函数。

分配!=施工,释放!=破坏

通常来说,要实现如上所述的许多数据结构,您不能将分配内存和构造元素视为一件不可分割的事情,同样,您也无法将释放内存和破坏元素视为一件不可分割的事情。

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

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

因此,无论如何,由于我已经编写了许多无法按照现有容器构建的通用标准C ++容器,因此我倾向于大量使用它。其中包括一个我在几十年前构建的小型矢量实现,以避免在常见情况下进行堆分配,还提供了内存效率较高的Trie(一次不会分配一个节点)。在这两种情况下,我都无法真正使用现有容器来实现它们,因此我不得不placement new避免在不必要的左右操作上不必要地调用构造函数和析构函数。

自然,如果您曾经使用过自定义分配器来分别分配对象(如空闲列表),那么通常也希望使用placement new,就像这样(基本示例不会打扰到异常安全性或RAII):

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

8

如果要构建内核,这将很有用-您将从磁盘或页表中读取的内核代码放在哪里?您需要知道跳到哪里。

或者在其他非常罕见的情况下,例如当您有分配的房间负载并且想要在彼此之间放置一些结构时。可以通过这种方式打包它们,而无需offsetof()运算符。但是,还有其他技巧。

我也相信某些STL实现会使用新的位置,例如std :: vector。他们以这种方式为2 ^ n个元素分配空间,而不必总是重新分配。


减少内存分配是使用内存分配的主要原因之一,也是“窍门”,例如将对象从磁盘加载到内存
lefticus

我不知道用C ++编写的内核。大多数内核都写在直C.
亚当罗森菲尔德

8
我学习过OS基础的操作系统是用C ++编写的: sweb.sourceforge.net
mstrobl,

8

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

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

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

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



7

我用它来存储带有内存映射文件的对象。
具体示例是一个图像数据库,该数据库处理了大量的大图像(超出了内存容量)。


7

我已经看到它用作“动态类型”指针轻微性能破解(在“内幕”部分中):

但是,这是我用来提高小型类型的快速性能的技巧:如果所保存的值可以放入void *内,我实际上不必理会分配新对象,而是使用new位置将其强制放入指针本身。


什么是如果持有的价值可以容纳一个void *里面是什么意思?总是可以将任何指针类型分配给void *。你能给我们举个例子吗?
anurag86

@ anurag86:在我的64位计算机上,一个void*占用8个字节。这是一个有点傻点八字节void*的一个字节bool。但它完全有可能实际的覆盖boolvoid*,很像union { bool b; void* v }。您需要某种方式来知道您称为a的东西void*实际上是a bool(或short,或a float等等)。我链接到的文章介绍了如何执行此操作。并且,为了回答最初的问题,放置new是用于boolvoid*期望的a 处创建(或其他类型)的功能(使用广播稍后用于获取/修改值)。
Max Lybbert

@ anurag86:这不是一回事,但是您可能会对标记指针感兴趣(en.wikipedia.org/wiki/Tagged_pointer)。
Max Lybbert


5

通常,新的安置用于摆脱“普通新”的分配成本。

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



4

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

我很少以类似的方式使用它,尽管它并不经常出现。但是,它对于C ++工具箱是一个有用的工具。


4

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


3

请参阅xll项目中的fp.h文件,该文件位于http://xll.codeplex.com。它为希望随身携带其尺寸的阵列解决了“编译器无用的笨拙”问题。

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

2

这是C ++就地构造函数的杀手级用途:对齐缓存行以及2边界的其他幂。这是我的超快速指针对齐算法,可以使用5个或更少的单周期指令来处理2个边界的任意幂

/* 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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.