“新定位”有什么用?

这里有人用过c++的“placement new”吗?如果有,为什么?在我看来,它只在内存映射硬件上有用。

209497 次浏览

如果你正在构建一个内核,它很有用——你把从磁盘或页表读取的内核代码放在哪里?你得知道该往哪里跳。

或者在其他非常罕见的情况下,比如当你有大量分配的空间,想要把几个结构放在彼此后面。它们可以这样打包,而不需要使用offset()操作符。不过,还有其他的技巧。

我也相信一些STL实现使用了新位置,比如std::vector。它们以这种方式为2^n个元素分配空间,而不需要总是realloc。

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

如果你想把分配和初始化分开,这是很有用的。STL使用放置new来创建容器元素。

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

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

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

DevX给出了很好的例子:

标准c++也支持放置 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的常见问题解答:是否有“位置删除”;?

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

我为此写了博客。

我们将它用于自定义内存池。简单介绍一下:

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


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


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

现在你可以将对象聚集在一个单独的内存区域中,选择一个非常快但不进行释放的分配器,使用内存映射,以及任何你希望通过选择池并将其作为参数传递给对象的放置new操作符来施加的语义。

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

一般来说,放置新是为了摆脱“正常新”的分配成本。

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

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

我能做的是预先分配一大块内存(大到足以容纳类可能需要的任何数量)。然后,一旦我在运行时弄清楚如何构造这些东西,就可以在我想要的地方使用放置new来构造对象。我知道我使用它的一个情况是帮助创建一个异构的循环缓冲区

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

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

我在类似的情况下使用过它,尽管它不经常出现。不过,它是c++工具箱中的一个有用工具。

我看到它被用作对“动态类型”指针的轻微性能改进(在“引擎盖下”一节中):

但这是我用来获得小类型的快速性能的棘手技巧:如果所持有的值可以放入void*中,我实际上不需要分配一个新对象,而是使用placement new将其强制到指针本身。

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

极客头头:答对了!你完全明白了,这正是它的完美之处。在许多嵌入式环境中,外部约束和/或整体使用场景迫使程序员将对象的分配与其初始化分开。总的来说,c++称之为“实例化”;但每当构造函数的动作必须显式调用而没有动态或自动分配时,放置new是一种方法。它也是定位固定在硬件组件地址(内存映射I/O)的全局c++对象的完美方法,或者对于任何静态对象,无论出于何种原因,必须驻留在固定地址。

查看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;

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

旧的C方法是使用memset()将所有元素设置为0。在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.
}

在序列化时(比如使用boost::serialization),放置new也非常有用。在c++的10年里,这只是我第二次需要新职位的情况(如果你包括面试的话,这是第三次:))。

我用它创建了一个Variant类(例如,一个对象可以表示一个单独的值,这个值可以是许多不同类型中的一个)。

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

因此,我分配了一个足够大的字节数组(例如sizeof(the_largest_data_type_I_support)),并使用placement new在该区域初始化适当的c++对象,当Variant被设置为持有该类型的值时。(当然,当切换到不同的数据类型时,我事先手动调用对象的析构函数)

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

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

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

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

实际上,实现任何类型的数据结构都需要分配比插入元素数量的最低要求更多的内存(即,除了每次分配一个节点的链接结构之外的任何数据结构)。

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

当你这样做时:

vector<Foo> vec;


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

... 其实也造不出一千个foo。它只是为它们分配/保留内存。如果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);

这里是c++ in-place构造函数的杀手级用法:对齐缓存线,以及其他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

在座有人用过c++的“placement new”吗?如果有,为什么?在我看来,它只在内存映射硬件上有用。

当需要复制(作为输出传递)时,它非常有用:

  1. 不可复制对象(例如:其中operator=()已被自动删除,因为类包含const成员)或
  2. non-trivially-copyable对象(其中使用memcpy()是未定义的行为)

在一个函数中。

这(从函数中获得这些不可复制或不可简单复制的对象)可以帮助单元测试该函数,允许您看到某个数据对象在被该函数处理后现在看起来是某种方式,或者它可以只是常规API的一部分,用于您认为合适的任何用途。让我们浏览这些例子,并详细解释我的意思以及如何“放置新”。可以用来解决这些问题。

TLDR;

注意:我已经测试了这个答案中的每一行代码。它的工作原理。它是有效的。它不违反c++标准。

新位置为:

  1. 在c++中,当operator=()(赋值操作符)为删除时,对=的替换,并且您需要“复制”;(实际上是复制-构造)一个因此不可复制的对象到给定的内存位置。
  2. 当对象不是trivially-copyable时,c++中memcpy()的替换,这意味着使用memcpy()来复制这个不可平凡复制的对象“可能是不明确的”;

重要提示:“不可复制”;对象不是真正不可复制的。它根本无法通过__ABC0操作符is all复制,该操作符是对类的底层__ABC1重载函数的调用。这意味着当你执行__ABC2时,实际发生的是对__ABC3的调用,而当你执行__ABC4时,实际发生的是A.operator=(B.operator=(C));。因此,“non-copyable"对象只能通过其他方法复制,例如通过类的copy 构造函数,因为该类同样没有operator=()方法。“new"位置;可用于调用类中可能存在的许多构造函数中的任何一个,以便将对象构造到所需的预分配内存位置。自“安置新”;语法允许调用类中的任何构造函数,这包括将一个类的现有实例传递给它,以便让placement new调用类的拷贝构造函数来从传入的对象复制构造一个新对象到内存中的另一个位置。复制—将一个对象构造到内存中的另一个位置……是副本。该操作将创建原始对象的副本。完成后,您可以有两个位于内存中两个不同位置的对象(实例),它们是字节相同的,字面上是字节对字节(取决于复制构造函数的实现)。根据定义,这是一种复制。只是没有使用类的operator=()方法完成而已

因此,可以将类定义为“不可复制的”;如果它没有operator=()方法,但仍然可以合法地,根据c++标准和c++提供的机制,安全且没有未定义的行为,使用它的复制构造函数放置new语法,如下所示。

提醒:下面的所有代码行都可以工作。 您可以在这里运行大部分代码,包括下面的许多代码块,尽管它可能需要一些注释/取消注释代码块,因为它没有被清晰地设置为单独的示例。

1. 不可复制对象是什么?

不可复制的对象不能用=操作符(operator=()函数)复制。就是这样!然而,它仍然可以被合法复制。请看上面真正重要的注释。

不可复制类例1:

在这里,复制构造是可以的,但是禁止复制,因为我们已经显式删除了赋值操作符。尝试执行nc2 = nc1;会导致以下编译时错误:

error: use of deleted function ‘NonCopyable1& NonCopyable1::operator=(const NonCopyable1&)’

下面是完整的例子:

#include <stdio.h>


class NonCopyable1
{
public:
int i = 5;


// Delete the assignment operator to make this class non-copyable
NonCopyable1& operator=(const NonCopyable1& other) = delete;
};


int main()
{
printf("Hello World\n");
    

NonCopyable1 nc1;
NonCopyable1 nc2;
nc2 = nc1;   // copy assignment; compile-time error!
NonCopyable1 nc3 = nc1; // copy constructor; works fine!


return 0;
}

不可复制类例2:

在这里,复制构造是可以的,但禁止复制,因为类包含const成员,不能写入(假设,因为显然有变通方法)。尝试执行nc2 = nc1;会导致以下编译时错误:

error: use of deleted function ‘NonCopyable1& NonCopyable1::operator=(const NonCopyable1&)’
note: ‘NonCopyable1& NonCopyable1::operator=(const NonCopyable1&)’ is implicitly deleted because the default definition would be ill-formed:
error: non-static const member ‘const int NonCopyable1::i’, can’t use default assignment operator

完整的例子:

#include <stdio.h>


class NonCopyable1
{
public:
const int i = 5; // classes with `const` members are non-copyable by default
};


int main()
{
printf("Hello World\n");
    

NonCopyable1 nc1;
NonCopyable1 nc2;
nc2 = nc1;   // copy assignment; compile-time error!
NonCopyable1 nc3 = nc1; // copy constructor; works fine!


return 0;
}

outputData = data;将导致编译失败,并出现上面最后一个例子中所示的错误消息!

#include <functional>


#include <stdio.h>


class NonCopyable1
{
public:
const int i; // classes with `const` members are non-copyable by default


// Constructor to custom-initialize `i`
NonCopyable1(int val = 5) : i(val)
{
// nothing else to do
}
};


// Some class which (perhaps asynchronously) processes data. You attach a
// callback, which gets called later.
// - Also, this may be a shared library over which you have no or little
// control, so you cannot easily change the prototype of the callable/callback
// function.
class ProcessData
{
public:
void attachCallback(std::function<void(void)> callable)
{
callback_ = callable;
}
    

void callCallback()
{
callback_();
}


private:
std::function<void(void)> callback_;
};


int main()
{
printf("Hello World\n");
    

NonCopyable1 outputData; // we need to receive back data through this object
printf("outputData.i (before) = %i\n", outputData.i); // is 5
    

ProcessData processData;
// Attach a lambda function as a callback, capturing `outputData` by
// reference so we can receive back the data from inside the callback via
// this object even though the callable prototype returns `void` (is a
// `void(void)` callable/function).
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
// NOT ALLOWED SINCE COPY OPERATOR (Assignment operator) WAS
// AUTO-DELETED since the class has a `const` data member!
outputData = data;
});
processData.callCallback();
// verify we get 999 here, NOT 5!
printf("outputData.i (after) = %i\n", outputData.i);


return 0;
}

一个解决方案:memcpy数据到outputData。这在C中是完全可以接受的,但在c++中并不总是可以。

Cppreference.com状态(强调添加):

如果对象可能重叠或不是TriviallyCopyable,,则未指定memcpy的行为,并且可能没有定义。 . 0

和:

< p > 笔记
普通可复制类型的对象是唯一可以使用std::memcpy安全复制或使用std::ofstream::write()/std::ifstream::read()序列化到二进制文件或从二进制文件序列化的c++对象,这些对象不是潜在重叠的子对象

(https://en.cppreference.com/w/cpp/string/byte/memcpy)

因此,为了安全起见,在使用memcpy()复制对象之前,先确保对象为是否可以简单地复制。替换上面的部分:

    processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
// NOT ALLOWED SINCE COPY OPERATOR (Assignment operator) WAS
// AUTO-DELETED since the class has a `const` data member!
outputData = data;
});

用这个。注意,这次使用memcpy()来复制数据,而使用std::is_trivially_copyable来确保在编译时,使用memcpy()来复制这种类型确实是安全的!:

    // (added to top)
#include <cstring>  // for `memcpy()`
#include <type_traits> // for `std::is_trivially_copyable<>()`


// Attach a lambda function as a callback, capturing `outputData` by
// reference so we can receive back the data from inside the callback via
// this object even though the callable prototype returns `void` (is a
// `void(void)` callable/function).
processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
static_assert(std::is_trivially_copyable<NonCopyable1>::value, "NonCopyable1 must "
"be a trivially-copyable type in order to guarantee that `memcpy()` is safe "
"to use on it.");
memcpy(&outputData, &data, sizeof(data));
});

示例程序输出,现在它可以编译&运行。它的工作原理!

Hello World
outputData.i (before) = 5
outputData.i (after) = 999

然而,为了更加安全,你应该在覆盖对象之前手动调用它的析构函数,如下所示:

最佳memcpy()解决方案:

    processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
static_assert(std::is_trivially_copyable<NonCopyable1>::value, "NonCopyable1 must "
"be a trivially-copyable type in order to guarantee that `memcpy()` is safe "
"to use on it.");
outputData.~NonCopyable1(); // manually call destructor before overwriting this object
memcpy(&outputData, &data, sizeof(data));
});

然而,如果上面的static_assert()失败了,那么你就不应该使用memcpy()。因此,一个始终安全并且更好的c++替代方法是使用&;placement new"。

这里,我们简单地复制构造dataoutputData占用的内存区域。这就是这次“定位”的新之处。语法帮了我们大忙!它不像new操作符那样动态分配内存。通常,new运营商 第一个在堆上动态分配内存,而outputData0通过调用对象的构造函数在该内存中构造一个对象。然而,放置新的不做分配部分。相反,它简单地跳过这一部分,并将一个对象构造到内存outputData1中,你必须是预先静态或动态分配该内存的人,并且你必须确保该对象的内存正确对齐(参见outputData2和outputData3以及这里的outputData4示例)(在这种情况下,因为我们显式地将outputData对象创建为对象,使用NonCopyable1 outputData;调用它的构造函数),你必须确保内存缓冲区/池足够大,以容纳你将要构建的数据。

泛型放置的新语法是这样的

// Call`T`'s specified constructor below, constructing it as an object right into
// the memory location pointed to by `ptr_to_buffer`. No dynamic memory allocation
// whatsoever happens at this time. The object `T` is simply constructed into this
// address in memory.
T* ptr_to_T = new(ptr_to_buffer) T(optional_input_args_to_T's_constructor);

在我们的例子中,它看起来像这样,调用NonCopyable1类的拷贝构造函数,我们已经在上面反复证明了即使删除赋值/复制操作符也有效:

// copy-construct `data` right into the address at `&outputData`, using placement new syntax
new(&outputData) NonCopyable1(data);

我们最终的attachCallback lambda现在看起来像这样,在memcpy()的位置放置了新语法。注意,确保对象可普通复制的检查不再需要。

= = =比;最好的c++解决方案全方位-避免MEMCPY复制构造直接到目标内存位置使用放置新:<====使用这个!= = = =

    processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
outputData.~NonCopyable1(); // manually call destructor before overwriting this object
// copy-construct `data` right into the address at `&outputData`, using placement new syntax
new(&outputData) NonCopyable1(data);


// Assume that `data` will be further manipulated and used below now, but we needed
// its state at this moment in time.


// Note also that under the most trivial of cases, we could have also just called
// out custom constructor right here too, like this. You can call whatever
// constructor you want!
// new(&outputData) NonCopyable1(999);


// ...
});

2. non-trivially-copyable对象是什么?

不可平凡复制的对象可能包含虚方法和东西,因为这可能导致类必须跟踪“vee指针”。(vptr)和"vee表"(vtbls),指向内存中正确的虚拟实现。在这里阅读更多信息:多布博士的《多态对象的存储布局》。然而,即使在这种情况下,只要你从同一个进程memcpy()ing到同一个进程(即:在同一个虚拟内存空间),而不是在过程,而不是从磁盘反序列化到RAM中,在我看来,memcpy()将技术工作好和生产没有缺陷(我对自己在少数的例子已经证明了这一点),但是它的技术似乎是行为而不是c++标准定义的,因此它是未定义行为,因此不能从编译器来编译器依靠100%,从c++的一个版本,所以…这是未定义的行为,在这种情况下你不应该memcpy()

换句话说,如果上面的static_assert(std::is_trivially_copyable<NonCopyable1>::value);检查失败,就不要使用memcpy()。你必须使用"定位"相反!

让静态断言失败的一种方法是简单地在NonCopyable1类的类定义中声明或定义一个自定义的复制/赋值操作符,如下所示:

// Custom copy/assignment operator declaration:
NonCopyable1& operator=(const NonCopyable1& other);


// OR:


// Custom copy/assignment operator definition:
NonCopyable1& operator=(const NonCopyable1& other)
{
// Check for, **and don't allow**, self assignment!
// ie: only copy the contents from the other object
// to this object if it is not the same object (ie: if it is not
// self-assignment)!
if(this != &other)
{
// copy all non-const members manually here, if the class had any; ex:
// j = other.j;
// k = other.k;
// etc.
// Do deep copy of data via any member **pointers**, if such members exist
}


// the assignment function (`operator=()`) expects you to return the
// contents of your own object (the left side), passed by reference, so
// that constructs such as `test1 = test2 = test3;` are valid!
// See this reference, from Stanford, p11, here!:
// http://web.stanford.edu/class/archive/cs/cs106b/cs106b.1084/cs106l/handouts/170_Copy_Constructor_Assignment_Operator.pdf
//      MyClass one, two, three;
//      three = two = one;
return *this;
}

(有关自定义复制构造函数、赋值操作符等的更多示例,以及“三规则”;和“五的规则”,见这里是我的hello world存储库和示例。)

所以,现在我们有了一个自定义赋值操作符,类不再是普通的可复制的,下面的代码:

    processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
static_assert(std::is_trivially_copyable<NonCopyable1>::value, "NonCopyable1 must "
"be a trivially-copyable type in order to guarantee that `memcpy()` is safe "
"to use on it.");
outputData.~NonCopyable1(); // manually call destructor before overwriting this object
memcpy(&outputData, &data, sizeof(data));
});

将产生以下错误:

main.cpp: In lambda function:
main.cpp:151:13: error: static assertion failed: NonCopyable1 must be a trivially-copyable type in order to guarantee that `memcpy()` is safe to use on it.
static_assert(std::is_trivially_copyable<NonCopyable1>::value, "NonCopyable1 must "
^~~~~~~~~~~~~

所以,你必须/(真的应该)使用"定位"相反,就像上面所描述的那样:

    processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
outputData.~NonCopyable1(); // manually call destructor before overwriting this object
// copy-construct `data` right into the address at `&outputData`, using placement new syntax
new(&outputData) NonCopyable1(data);
});

更多关于预先分配缓冲区/内存池的信息

如果你真的只是想使用placement new直接复制-construct到内存池/共享内存/预分配的对象空间中,那么就没有必要使用NonCopyable1 outputData;将一个无用的实例构造到那个稍后必须销毁的内存中。相反,您可以只使用字节的内存池。格式如下:

(来自:"Placement new"section: https://en.cppreference.com/w/cpp/language/new)

// within any scope...
{
char buf[sizeof(T)];  // Statically allocate memory large enough for any object of
// type `T`; it may be misaligned!
// OR, to force proper alignment of your memory buffer for your object of type `T`,
// you may specify memory alignment with `alignas()` like this instead:
alignas(alignof(T)) char buf[sizeof(T)];
T* tptr = new(buf) T; // Construct a `T` object, placing it directly into your
// pre-allocated storage at memory address `buf`.
tptr->~T();           // You must **manually** call the object's destructor.
}                         // Leaving scope here auto-deallocates your statically-allocated
// memory `buf`.

所以,在上面的例子中,这个静态分配的输出缓冲区:

// This constructs an actual object here, calling the `NonCopyable1` class's
// default constructor.
NonCopyable1 outputData;

会变成这样:

// This is just a statically-allocated memory pool. No constructor is called.
// Statically allocate an output buffer properly aligned, and large enough,
// to store 1 single `NonCopyable1` object.
alignas(alignof(NonCopyable1)) uint8_t outputData[sizeof(NonCopyable1)];
NonCopyable1* outputDataPtr = (NonCopyable1*)(&outputData[0]);

然后通过outputDataPtr指针读取outputData对象的内容。

前方法(NonCopyable1 outputData;)是最好的如果存在一个这个类的构造函数不需要输入参数时你没有访问这个缓冲区的创建,而且如果你只打算将这一数据类型存储到缓冲区,而后者uint8_t缓冲区方法是最好的如果你)没有访问的所有输入参数甚至构造对象所需的位置需要创建这个缓冲区,或者B)如果你计划在这个内存池中存储多种数据类型,可能是为了在线程、模块、进程等之间以一种联合的方式进行通信。

更多关于c++的内容,以及为什么它让我们在这种情况下跳过这些圈

所以,这整个“安置新”;c++中的东西,以及对它的需求,花了我大量的学习和很长时间来理解它。经过思考,我想到C语言范式(我来自哪里)是手动分配一些内存,然后将一些东西插入其中。在处理静态和动态内存分配时,这些都是单独的操作(记住:你甚至不能为structs设置默认值!)。没有构造函数或析构函数的概念,即使是获得一个基于作用域的析构函数的__attribute__((__cleanup__(my_variable)))0(当变量退出给定作用域时自动调用)也是一件令人头疼的事情,需要一些奇特的gcc扩展__attribute__((__cleanup__(my_variable))) magic __attribute__((__cleanup__(my_variable)))1。然而,从一个对象任意复制到另一个对象是__attribute__((__cleanup__(my_variable)))2,只是复制周围的对象!这与__attribute__((__cleanup__(my_variable)))3形成对比,后者是__attribute__((__cleanup__(my_variable)))4。这个范例集中于准备使用__attribute__((__cleanup__(my_variable)))5的对象。为了实现这一点,它们依赖__attribute__((__cleanup__(my_variable)))6和__attribute__((__cleanup__(my_variable)))7。这意味着像这样创建一个对象:NonCopyable1 data(someRandomData);,不仅为该对象__attribute__((__cleanup__(my_variable)))8,而且还__attribute__((__cleanup__(my_variable)))9,并将该对象构造(放置)到该内存中。它试图在一件事上做多件事。因此,在c++中,memcpy()和赋值操作符(=;operator=()函数)明显受到c++性质的限制。这就是为什么我们必须通过这个奇怪的“复制构造我的对象到一个给定的内存位置通过放置新”的圆环。进程,而不是仅仅创建一个变量,然后稍后将内容复制到它中,或者如果它包含const成员,则稍后将内容memcpy()ing到它中,就像我们在C中所做的那样,c++确实试图强制RAII,这是他们实现它的部分方式。

你可以使用std::optional<>::emplace()代替

从c++ 17开始,你也可以使用std::optional<>作为包装器。各种容器和包装器的现代c++ emplace()函数做的是我们上面手动做的“placement new”;(另见我的答案是和关于std::vector<T,Allocator>::emplace_back "通常使用placement-new构造元素in-place"的引用)。

std::optional静态分配一个足够大的缓冲区给你想要放入其中的对象。然后,它要么存储该对象,要么存储std::nullopt(与{}相同),这意味着它不存储该对象。要用另一个对象替换其中的一个对象,只需调用std::optional对象上的emplace()方法。它的作用如下:

就地构造包含的值。如果*this在调用之前已经包含一个值,则通过调用其析构函数来销毁所包含的值。

因此,它首先手动调用已在其内部的现有对象的析构函数,如果已有对象已在其内部,则它执行与“放置new"复制构造一个新对象(您提供的对象)到该内存空间。

那么,这个输出缓冲区:

NonCopyable1 outputData;


// OR


alignas(alignof(NonCopyable1)) uint8_t outputData[sizeof(NonCopyable1)];
NonCopyable1* outputDataPtr = (NonCopyable1*)(&outputData[0]);

现在变成这样:

# include <optional>


std::optional<NonCopyable1> outputData = std::nullopt;

而这个“安置新”;复制构造到输出缓冲区:

processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
outputData.~NonCopyable1(); // manually call destructor before overwriting this object
// copy-construct `data` right into the address at `&outputData`, using placement new syntax
new(&outputData) NonCopyable1(data);
});

现在变成这个emplace()ment,将新数据放入缓冲区。注意,对析构函数的手动调用是不再需要,因为对我们来说是std::optional<>::emplace() 已经处理对任何已经存在的对象调用析构函数 !:

processData.attachCallback([&outputData]()
{
int someRandomData = 999;
NonCopyable1 data(someRandomData);
// emplace `data` right into the `outputData` object
outputData.emplace(data);
});

现在,要从outputData中获取数据,只需使用*对其进行解引用,或对其调用.value()。例:

// verify we get 999 here!
if (outputData.has_value())
{
printf("(*outputData).i (after) = %i\n", (*outputData).i);
// OR
printf("outputData.value().i (after) = %i\n", outputData.value().i);
}
else
{
printf("outputData.has_value() is false!");
}

样例输出:

Hello World
(*outputData).i (after) = 999
outputData.value().i (after) = 999

在这里运行完整的示例代码

引用,另外,优秀的阅读:

  1. *****+[一些最有用和最简单的"安置新"我从未见过这样的例子!] https://www.geeksforgeeks.org/placement-new-operator-cpp/
  2. https://en.cppreference.com/w/cpp/language/new—>参见&;位置new&;部分和示例在这里!(我帮助编写了示例)。
  3. 我如何使这个c++对象不可复制?< / >
  4. [真正重要的一点是,调用放置新行在构造对象时调用对象的构造函数!]第3行(Fred* f = new(place) Fred();)本质上只是调用构造函数Fred::Fred()。这意味着Fred构造函数中的this指针将等于__abc4。http://www.cs.technion.ac.il/users/yechiel/c++-faq/placement-new.html
    1. http://www.cs.technion.ac.il/users/yechiel/c++-faq/memory-pools.html
  5. 博士。多布的“多态对象的存储布局”
  6. [好的pre- c++ 11 c++“三的规则”介绍]http://web.stanford.edu/class/archive/cs/cs106b/cs106b.1084/cs106l/handouts/170_Copy_Constructor_Assignment_Operator.pdf
  7. 我的“hello world”;示例和存储库,演示自定义复制构造函数,赋值操作符等,与c++“三规则”相关;/“五法则”;/“零原则”;/“0/3/5规则”:https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/cpp/copy_constructor_and_assignment_operator/copy_constructor_and_assignment_operator.cpp
  8. [微软关于c++ 17的std::optional<>类型使用的优秀的编写
  9. [相关的,since "placement new"这显然也解决了这个问题,因为这个问题是我的大多数解决方案和示例背后的关键和驱动力!] Const成员和赋值操作符。如何避免未定义的行为?
我也有个主意。 c++确实有zero-overhead原则。 但是异常不遵循这个原则,所以有时它们会被编译器开关关闭

让我们来看看这个例子:

#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 block,并且无法处理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

乌利希期刊指南 @Useless写了一个注释,它向我的视图打开了新(nothrow)的存在,在这种情况下应该使用它,但不是我之前写的方法。请不要使用我之前写的代码。对不起。

我还有一个想法(它对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给出了以下输出:

max_align_t: 16
a: 0x7ffd45e6f000
ptr: 0x1fe3ec0

ptr没有正确对齐。

在c++ 17标准中,GCC给出了以下输出:

max_align_t: 16
a: 0x7ffc924f6000
ptr: 0x9f6000

ptr正确对齐。

据我所知,c++标准在c++ 17之前不支持过对齐的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对齐

max_align_t: 16
a: 0x7ffe56b57000
ptr: 0x2416000