“放置新”有什么用途?

时间:2008-10-21 16:34:53

标签: c++ memory-management new-operator placement-new

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

24 个答案:

答案 0 :(得分:336)

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

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

DevX提供good example

  

标准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

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

放置新

时取消分配

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

答案 1 :(得分:55)

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

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

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

答案 2 :(得分:48)

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

答案 3 :(得分:32)

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

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

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

答案 4 :(得分:23)

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

无耻插件:我在博客上发表了here

答案 5 :(得分:12)

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

答案 6 :(得分: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数据类型时,预先删除位置)

答案 7 :(得分: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 :(得分:8)

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

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

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

答案 9 :(得分:8)

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

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

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

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

答案 10 :(得分:8)

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

答案 11 :(得分:7)

它由std::vector<>使用,因为std::vector<>通常会分配比objectsvector<>更多的内存。

答案 12 :(得分:7)

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

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

当你这样做时:

vector<Foo> vec;

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

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

分配!=构建,释放!=销毁

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

这些想法之间必须存在分离,以避免不必要地左右调用构造函数和析构函数,这就是为什么标准库将std::allocator的概念分开的原因(这不是'{1}}构造或销毁元素时,它分配/释放内存*)远离使用它的容器,它使用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);

答案 13 :(得分:7)

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

  

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

答案 14 :(得分:7)

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

答案 15 :(得分:6)

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

答案 16 :(得分:5)

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

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

答案 17 :(得分:5)

答案 18 :(得分:4)

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

答案 19 :(得分:4)

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

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

答案 20 :(得分: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;

答案 21 :(得分:2)

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

k

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

答案 22 :(得分:1)

我也有一个想法。 C++ 确实有 zero-overhead principle。 但是异常不遵循这个原则,所以有时会被编译器开关关闭。

让我们看看这个例子:

#include <new>
#include <cstdio>
#include <cstdlib>

int main() {
    struct A {
        A() {
            printf("A()\n");
        }
        ~A() {
            printf("~A()\n");
        }
        char data[1000000000000000000] = {}; // some very big number
    };

    try {
        A *result = new A();
        printf("new passed: %p\n", result);
        delete result;
    } catch (std::bad_alloc) {
        printf("new failed\n");
    }
}

我们在这里分配一个大结构体,检查分配是否成功,然后删除。

但是如果我们关闭了异常,我们就不能使用 try 块,也无法处理 new[] 失败。

那我们怎么做呢?方法如下:

#include <new>
#include <cstdio>
#include <cstdlib>

int main() {
    struct A {
        A() {
            printf("A()\n");
        }
        ~A() {
            printf("~A()\n");
        }
        char data[1000000000000000000] = {}; // some very big number
    };

    void *buf = malloc(sizeof(A));
    if (buf != nullptr) {
        A *result = new(buf) A();
        printf("new passed: %p\n", result);
        result->~A();
        free(result);
    } else {
        printf("new failed\n");
    }
}
  • 使用简单的 malloc
  • 以C方式检查是否失败
  • 如果成功,我们将使用新版位
  • 手动调用析构函数(我们不能只调用delete)
  • 免费调用,因为我们调用了 malloc

UPD @Useless 写了一条评论,让我看到 new(nothrow) 的存在,应该在这种情况下使用,但不是我之前写的方法。请不要使用我之前写的代码。对不起。

答案 23 :(得分:0)

我还有一个想法(它对 C++11 有效)。

让我们看下面的例子:

#include <cstddef>
#include <cstdio>

int main() {
    struct alignas(0x1000) A {
        char data[0x1000];
    };

    printf("max_align_t: %zu\n", alignof(max_align_t));

    A a;
    printf("a: %p\n", &a);

    A *ptr = new A;
    printf("ptr: %p\n", ptr);
    delete ptr;
}

使用 C++11 标准,GCC 给出以下 output

max_align_t: 16
a: 0x7ffd45e6f000
ptr: 0x1fe3ec0

ptr 未正确对齐。

在 C++17 标准及更高版本中,GCC 给出以下 output

max_align_t: 16
a: 0x7ffc924f6000
ptr: 0x9f6000

ptr 已正确对齐。

据我所知,在 C++17 出现之前,C++ 标准不支持过度对齐的 new,如果您的结构对齐大于 max_align_t,您可能会遇到问题。 要在 C++11 中绕过此问题,您可以使用 aligned_alloc

#include <cstddef>
#include <cstdlib>
#include <cstdio>
#include <new>

int main() {
    struct alignas(0x1000) A {
        char data[0x1000];
    };

    printf("max_align_t: %zu\n", alignof(max_align_t));

    A a;
    printf("a: %p\n", &a);

    void *buf = aligned_alloc(alignof(A), sizeof(A));
    if (buf == nullptr) {
        printf("aligned_alloc() failed\n");
        exit(1);
    }
    A *ptr = new(buf) A();
    printf("ptr: %p\n", ptr);
    ptr->~A();
    free(ptr);
}

ptr 在这种情况下是 aligned

max_align_t: 16
a: 0x7ffe56b57000
ptr: 0x2416000