这里有人用过 C++ 的“placement new”吗?如果有,是为了什么?在我看来,它只对内存映射硬件有用。
25 回答
Placement 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
您可能还想确保在关键代码的某个部分(例如,在起搏器执行的代码中)不会出现分配失败。在这种情况下,您可能希望更早地分配内存,然后在临界区中使用新位置。
重新分配安置新
您不应该取消分配正在使用内存缓冲区的每个对象。相反,您应该只删除 [] 原始缓冲区。然后,您必须手动调用类的析构函数。有关这方面的好建议,请参阅 Stroustrup 的常见问题解答:是否存在“展示位置删除”?
我们将它与自定义内存池一起使用。只是一个草图:
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, ...);
现在您可以将对象聚集在一个内存区域中,选择一个非常快但不释放的分配器,使用内存映射以及您希望通过选择池并将其作为参数传递给对象的位置来施加的任何其他语义新运营商。
如果您想将分配与初始化分开,这很有用。STL 使用placement new 来创建容器元素。
我在实时编程中使用过它。我们通常不想在系统启动后执行任何动态分配(或解除分配),因为无法保证需要多长时间。
我能做的是预先分配一大块内存(大到足以容纳类可能需要的任何数量)。然后,一旦我在运行时弄清楚如何构造这些东西,就可以使用placement new 在我想要的位置构造对象。我知道我使用它的一种情况是帮助创建异构循环缓冲区。
这当然不适合胆小的人,但这就是为什么他们使它的语法有点粗糙的原因。
我用它来构造通过 alloca() 在堆栈上分配的对象。
无耻的插件:我在这里写过博客。
头怪:宾果!你完全明白了——这正是它的完美之处。在许多嵌入式环境中,外部约束和/或整体使用场景迫使程序员将对象的分配与其初始化分开。综合起来,C++ 称之为“实例化”;但是只要构造函数的操作必须在没有动态或自动分配的情况下显式调用时,placement new 就是这样做的方法。它也是定位固定到硬件组件地址(内存映射 I/O)的全局 C++ 对象的完美方式,或者对于无论出于何种原因必须驻留在固定地址的任何静态对象的位置。
我用它来创建一个 Variant 类(即一个可以表示单个值的对象,该值可以是多种不同类型之一)。
如果 Variant 类支持的所有值类型都是 POD 类型(例如 int、float、double、bool),那么标记的 C 样式联合就足够了,但是如果您希望某些值类型是 C++ 对象(例如 std::string),C 联合功能不会这样做,因为非 POD 数据类型可能不会被声明为联合的一部分。
因此,我分配了一个足够大的字节数组(例如 sizeof(the_largest_data_type_I_support)),并在 Variant 设置为保存该类型的值时使用placement new 来初始化该区域中的适当C++ 对象。(当然,当切换到不同的数据类型时,我会事先手动调用对象的析构函数)
实际上,实现任何类型的数据结构都需要分配比插入元素数量的最低要求更多的内存(即,除了一次分配一个节点的链接结构之外的任何数据结构)。
unordered_map
使用、vector
或等容器deque
。这些都分配了比您迄今为止插入的元素所需的最低内存更多的内存,以避免每次插入都需要堆分配。让我们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);
序列化时放置 new 也非常有用(比如使用 boost::serialization)。在 10 年的 c++ 中,这只是我需要安置新的第二个案例(如果你包括面试,第三个 :))。
如果您正在构建内核,这很有用 - 您将从磁盘或页表中读取的内核代码放在哪里?你需要知道跳到哪里。
或者在其他非常罕见的情况下,例如当您有大量分配的空间并且想要将一些结构放在一起时。它们可以以这种方式打包,而不需要使用 offsetof() 运算符。不过,还有其他技巧。
我也相信一些 STL 实现使用了新的放置,比如 std::vector。他们以这种方式为 2^n 个元素分配空间,并且不需要总是重新分配。
当您想要重新初始化全局或静态分配的结构时,它也很有用。
旧的 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.
}
我认为任何答案都没有强调这一点,但是新放置的另一个很好的例子和用法是减少内存碎片(通过使用内存池)。这在嵌入式和高可用性系统中特别有用。在最后一种情况下,它特别重要,因为对于必须运行 24/365 天的系统来说,没有碎片非常重要。这个问题与内存泄漏无关。
即使使用了非常好的 malloc 实现(或类似的内存管理功能),也很难长时间处理碎片。在某些时候,如果您没有巧妙地管理内存预留/释放调用,您最终可能会出现许多难以重用的小间隙(分配给新的预留)。因此,在这种情况下使用的解决方案之一是使用内存池预先为应用程序对象分配内存。之后,每次您需要某个对象的内存时,您只需使用新位置在已保留的内存上创建一个新对象。
这样,一旦您的应用程序启动,您就已经保留了所有需要的内存。所有新的内存保留/释放都进入分配的池(您可能有多个池,一个用于每个不同的对象类)。在这种情况下不会发生内存碎片,因为不会有间隙,并且您的系统可以运行很长一段时间(数年)而不会受到碎片的影响。
我在实践中特别为 VxWorks RTOS 看到了这一点,因为它的默认内存分配系统受到很多碎片的影响。所以项目中基本禁止通过标准的new/malloc方法分配内存。所有的内存预留都应该去一个专用的内存池。
我用它来存储带有内存映射文件的对象。
具体的例子是一个图像数据库,它处理了大量的大图像(超过内存容量)。
使用它是std::vector<>
因为通常分配的std::vector<>
内存比.objects
vector<>
我已经看到它被用作“动态类型”指针的轻微性能黑客(在“引擎盖下”部分):
但这是我用来为小型类型获得快速性能的棘手技巧:如果所持有的值可以放入 void* 中,我实际上并不费心分配新对象,我使用placement new 将其强制放入指针本身.
我用它来创建基于包含从网络接收到的消息的内存的对象。
通常,放置新用于摆脱“正常新”的分配成本。
我使用它的另一个场景是我想访问指向仍然要构造的对象的指针,以实现每个文档的单例。
在使用共享内存以及其他用途时,它可能会很方便......例如: http: //www.boost.org/doc/libs/1_51_0/doc/html/interprocess/synchronization_mechanisms.html#interprocess.synchronization_mechanisms.conditions。 conditions_anonymous_example
我遇到的一个地方是在容器中,它分配一个连续的缓冲区,然后根据需要用对象填充它。如前所述,std::vector 可能会这样做,而且我知道某些版本的 MFC CArray 和/或 CList 会这样做(因为那是我第一次遇到它的地方)。缓冲区过度分配方法是一种非常有用的优化方法,并且放置 new 几乎是在这种情况下构造对象的唯一方法。它有时也用于在直接代码之外分配的内存块中构造对象。
我以类似的身份使用过它,尽管它不经常出现。不过,它对于 C++ 工具箱来说是一个有用的工具。
脚本引擎可以在本机接口中使用它来从脚本分配本机对象。有关示例,请参见 Angelscript (www.angelcode.com/angelscript)。
请参阅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;
这是 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
我也有一个想法。C++ 确实有零开销原则。但是异常不遵循这个原则,所以有时会用编译器开关来关闭它们。
让我们看一下这个例子:
#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");
}
}
我们在这里分配一个大的struct,检查分配是否成功,然后删除。
但是如果我们关闭了异常,我们就不能使用 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方式失败
- 如果成功,我们使用placement new
- 手动调用析构函数(我们不能只调用delete)
- 免费调用,因为我们调用了 malloc
UPD @Useless写了一条评论,让我看到了new(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
这里有人用过 C++ 的“placement new”吗?如果有,是为了什么?在我看来,它只对内存映射硬件有用。
在需要复制时非常有用(作为输出传递):
- 不可复制的对象(例如:
operator=()
由于类包含const
成员而被自动删除的位置)或 - 不可复制的对象(其中 using
memcpy()
是未定义的行为)
...从一个函数中。
这(从函数中获取这些不可复制或不可复制的对象)可以帮助对该函数进行单元测试,方法是让您看到某个数据对象在被该函数处理后现在看起来是某种方式,或者它可以简单地成为您正常 API 的一部分,用于您认为合适的任何用途。让我们回顾一下这些例子,详细解释我的意思以及如何使用“新布局”来解决这些问题。
TLDR;
注意:我已经测试了这个答案中的每一行代码。有用。这是有效的。它不违反 C++ 标准。
新的展示位置是:
- 在 C++ 中替换
=
whenoperator=()
(赋值运算符)被删除,并且您需要“复制”(实际上是复制构造)一个因此不可复制的对象到给定的内存位置。 - C++ 中的替换
memcpy()
当您的对象不是trivially-copyable时,这意味着使用memcpy()
来复制这个 non-trivially-copyable 对象“可能是 undefined”。
重要提示:“不可复制”对象并不是真正不可复制的。它根本不能通过=
operator is all 复制,这是对类的底层operator=()
重载函数的调用。这意味着当你这样做时B = C;
,实际发生的是对的调用B.operator=(C);
,而当你这样做时A = B = C;
,实际发生的是调用A.operator=(B.operator=(C));
。因此,“不可复制”对象只能通过其他方式复制,例如通过类的复制构造函数,因为同样,该类没有operator=()
方法。“Placement new”可用于调用类中可能存在的许多构造函数中的任何一个,以便将对象构造到所需的预分配内存位置。由于“placement new”语法允许调用类中的任何构造函数,这包括将一个现有的类实例传递给它,以便让placement new调用类的复制构造函数从传入的对象复制构造一个新对象到内存中的另一个位置。将一个对象复制构建到内存中的另一个位置……是一个副本。此操作会创建该原始对象的副本。完成后,您可以拥有两个对象(实例),它们是字节相同的,字面上是逐字节的(取决于您的复制构造函数的实现),位于内存中的两个不同位置。根据定义,那是一个副本。只是没有使用类的operator=()
方法完成。
因此,如果一个类没有operator=()
方法,则可以将其定义为“不可复制”,但根据 C++ 标准和 C++ 提供的机制,它仍然是非常可复制的,合法地,安全且没有未定义的行为,使用它的复制构造函数和放置新语法,如下所示。
提醒:下面的所有代码行都有效。 您可以在此处运行大部分代码,包括。下面的许多代码块,尽管它可能需要一些注释/取消注释的代码块,因为它没有干净地设置为单独的示例。
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 的行为并且可能未定义。
和:
注意
不是潜在重叠子对象的普通可复制类型的对象是唯一可以使用 / 安全地复制或序列化到二进制文件的 C ++std::memcpy
对象。std::ofstream::write()
std::ifstream::read()
(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”。
在这里,我们只是简单地复制构造data
到 . 占用的内存区域outputData
。这就是“放置新”语法为我们所做的!它不像new
操作员通常那样动态分配内存。通常,new
运算符 首先在堆上动态分配内存,然后通过调用对象的构造函数将对象构造到该内存中。但是,placement new 不会执行分配部分。相反,它只是跳过该部分并在您指定的地址处将对象构造到内存中!您必须是事先静态或动态分配该内存的人,并且您必须确保为该对象正确对齐内存(请参阅alignof
和alignas
以及此处的Placement 新示例)(在这种情况下,因为我们明确地将对象创建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. 什么是不可复制的对象?
一个不可复制的对象可能是包含虚拟方法和事物的对象,因为这可能导致类必须跟踪“vee 指针”(vptr
)和“vee 表”(vtbl
s),以指向正确的虚拟实现记忆。在此处阅读更多相关信息:Dobb 博士的“多态对象的存储布局”。但是,即使在这种情况下,只要您是memcpy()
从同一个进程到同一个进程(即:在同一个虚拟内存空间内),而不是在进程之间,也不是从磁盘反序列化到 RAM,在我看来那memcpy()
从技术上讲可以正常工作并且不会产生错误(我已经在一些示例中向自己证明了这一点),但从技术上讲,它似乎是 C++ 标准未定义的行为,因此它是未定义的行为,因此它从编译器到编译器,从一个版本的 C++ 到下一个版本,都不能 100% 依赖,所以……这是未定义的行为,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 " ^~~~~~~~~~~~~
所以,你必须/(真的应该)改用“placement new”,就像前面描述的那样:
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来复制构造到内存池/共享内存/预分配的对象空间,那么就没有必要使用NonCopyable1 outputData;
在我们必须稍后销毁的内存中构造一个无用的实例反正。相反,您可以只使用字节内存池。格式是这样的:
(来自:“安置新”部分: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]);
然后你会outputData
通过outputDataPtr
指针读取对象的内容。
前一种方法 ( NonCopyable1 outputData;
) 是最好的,如果此类的构造函数不需要在创建此缓冲区时您无权访问的输入参数,并且如果您只打算将这一种数据类型存储到此缓冲区中,而后一种uint8_t
缓冲区方法是最好的,如果您要么 A)无法访问甚至在您需要创建此缓冲区的位置构造对象所需的所有输入参数,或者 B)如果您计划将多种数据类型存储到这个内存池,可能用于以联合方式在线程、模块、进程等之间进行通信。
更多关于 C++ 以及为什么它让我们在这种情况下跳过这些障碍
因此,C++ 中的这整个“放置新”事物,以及对它的需求,花了我大量的研究和很长时间来围绕它。想了想,我突然想到,C (我来自哪里)的范式是手动分配一些内存,然后把一些东西塞进去。在处理静态和动态内存分配时,这些都是单独struct
的操作(请记住:您甚至不能为s 设置默认值!)。没有构造函数或析构函数的概念,甚至获得基于作用域的析构函数的行为,该析构函数在变量退出给定作用域时自动调用,这很麻烦,并且需要一些花哨的 gcc 扩展__attribute__((__cleanup__(my_variable)))
魔法,因为我在我的回答中展示. 然而,任意从一个对象复制到另一个对象是非常容易的。只需复制周围的对象!这与C++ 的范式形成对比,即RAII(资源获取即初始化)。这种范式侧重于对象在创建时就可以立即使用。为了实现这一点,它们依赖于构造函数和析构函数。这意味着像这样创建一个对象:NonCopyable1 data(someRandomData);
,不仅为该对象分配内存,它还调用对象的构造函数并将该对象构造(放置)到该内存中。它试图将多件事合二为一。所以,在 C++ 中,memcpy()
并且赋值运算符 ( =
; AKA: operator=()
function) 更明显地受到 C++ 的性质的限制。这就是为什么我们必须在 C++ 中完成这个奇怪的“通过放置新的位置将我的对象复制到给定的内存位置”过程,而不是仅仅创建一个变量并稍后将内容复制到其中,或者稍后将内容memcpy()
写入其中如果它包含一个const
成员,就像我们在 C 中所做的那样。C++ 确实试图强制执行 RAII,这部分是他们这样做的方式。
您可以std::optional<>::emplace()
改用
从 C++17 开始,您也可以将std::optional<>
其用作包装器。各种容器和包装器的现代 C++emplace()
函数执行我们在上面使用“placement new”手动执行的操作(另请参见我的回答here和关于std::vector<T,Allocator>::emplace_back
“通常使用placement-new就地构造元素”的引用)。
std::optional
静态分配一个足够大的缓冲区,以容纳您要放入其中的对象。然后它要么存储该对象,要么存储一个std::nullopt
(与 相同{}
),这意味着它不保存该对象。要将其中的一个对象替换为另一个对象,只需调用该对象的emplace()
方法即可std::optional
。这将执行以下操作:
就地构造包含的值。如果
*this
在调用之前已经包含一个值,则通过调用其析构函数来销毁包含的值。
因此,它首先手动调用其中已经存在的现有对象的析构函数,如果现有对象已经在其中,那么它相当于“放置新”来复制构造一个新对象(您提供它)到那个内存空间。
所以,这个输出缓冲区:
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()
缓冲区中的新数据。请注意,不再需要手动调用析构函数,因为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
参考资料和附加,优秀的阅读:
- *****+[一些我见过的最有用和最简单的“放置新”示例!] https://www.geeksforgeeks.org/placement-new-operator-cpp/
- [很好的例子] https://en.cppreference.com/w/cpp/language/new --> 请参阅“新放置”部分和此处的示例!(我帮助编写了示例)。
- 如何使这个 C++ 对象不可复制?
- [非常重要的一点是,在构造对象时调用放置新行会调用对象的构造函数!:第 3 行(
Fred* f = new(place) Fred();
)本质上只是调用构造函数Fred::Fred()
。这意味着“构造函数this
中的指针Fred
将等于place
”。] http://www.cs.technion.ac.il/users/yechiel/c++-faq/placement-new.html - Dobb博士的《多态对象的存储布局》
- [C++“三法则”的良好的 pre-C++11 介绍] http://web.stanford.edu/class/archive/cs/cs106b/cs106b.1084/cs106l/handouts/170_Copy_Constructor_Assignment_Operator.pdf
- 我的“hello world”示例和存储库,演示自定义复制构造函数、赋值运算符等,与 C++“三规则”/“五规则”/“零规则”/“0/3/5 规则”相关“:https ://github.com/ElectricRCAAircraftGuy/eRCaGuy_hello_world/blob/master/cpp/copy_constructor_and_assignment_operator/copy_constructor_and_assignment_operator.cpp
- [微软关于 C++17 类型使用的优秀
std::optional<>
文章] https://devblogs.microsoft.com/cppblog/stdoptional-how-when-and-why/ - [相关,因为“放置新”也非常清楚地解决了这个问题,因为这个问题是我的大多数解决方案和示例背后的症结和驱动力!] const 成员和赋值运算符。如何避免未定义的行为?