这里有人用过c++的“placement new”吗?如果有,为什么?在我看来,它只在内存映射硬件上有用。
当前回答
我曾看到它被用作“动态类型”指针的轻微性能hack(在“引擎盖下”一节中):
但这是我用来获得小类型的快速性能的棘手技巧:如果所持有的值可以放入void*中,我实际上不需要分配一个新对象,而是使用placement new将其强制到指针本身。
其他回答
这里有人用过c++的“placement new”吗?如果有,为什么?在我看来,它只在内存映射硬件上有用。
当需要复制(作为输出传递)时,它非常有用:
不可复制的对象(例如:operator=()被自动删除,因为该类包含一个const成员)或 不可平凡复制的对象(其中使用memcpy()是未定义的行为)
在一个函数中。
这(从函数中获得这些不可复制或不可简单复制的对象)可以帮助单元测试该函数,允许您看到某个数据对象在被该函数处理后现在看起来是某种方式,或者它可以只是常规API的一部分,用于您认为合适的任何用途。让我们通过这些例子来详细解释我的意思以及如何使用“新定位”来解决这些问题。
TLDR;
注意:我已经测试了这个答案中的每一行代码。它的工作原理。它是有效的。它不违反c++标准。
新位置为:
在c++中,当operator=()(赋值操作符)被删除时,for =的替换,你需要“复制”(实际上是复制-构造)一个因此不可复制的对象到给定的内存位置。 在c++中,当你的对象不可简单复制时,memcpy()的替换,这意味着使用memcpy()来复制这个不可简单复制的对象“可能是未定义的”。
Important: a "non-copyable" object is NOT truly non-copyable. It is simply not copyable via the = operator is all, which is a call to a class's underlying operator=() overload function. This means that when you do B = C;, what is actually happening is a call to B.operator=(C);, and when you do A = B = C;, what is actually happening is A.operator=(B.operator=(C));. Therefore, "non-copyable" objects are only copyable via other means, such as via the class's copy constructor, since, again, the class has no operator=() method. "Placement new" can be used to call any one of the many constructors which may exist in a class in order to construct an object into a desired pre-allocated memory location. Since "placement new" syntax allows for calling any constructor in a class, this includes passing it an existing instance of a class in order to have placement new call a class's copy-constructor to copy-construct a new object from the passed-in object into another place in memory. Copy-constructing one object into another place in memory...is a copy. This action creates a copy of that original object. When done, you CAN have two objects (instances), that are byte-identical, literally byte for byte (depending on your copy constructor's implementation), located in two separate places in memory. That, by definition, is a copy. It just wasn't done using the class's operator=() method is all.
因此,如果一个类没有operator=()方法,就可以将它定义为“不可复制的”,但它仍然是非常可复制的,合法的,根据c++标准和c++提供的机制,使用它的复制构造函数和放置新语法,安全且没有未定义的行为,如下所示。
提醒:下面的所有代码行都可以工作。您可以在这里运行大部分代码,包括下面的许多代码块,尽管它可能需要一些注释/取消注释代码块,因为它没有清晰地设置为单独的示例。
1. 什么是不可复制对象?
不可复制的对象不能使用=操作符(operator=()函数)进行复制。就是这样!然而,它仍然可以被合法复制。请看上面真正重要的注释。
不可复制类例1:
在这里,复制构造是可以的,但是禁止复制,因为我们已经显式删除了赋值操作符。试着做nc2 = nc1;导致编译时错误:
错误:使用已删除的函数' 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;导致编译时错误:
错误:使用已删除的函数' NonCopyable1& NonCopyable1::operator=(const NonCopyable1&) ' 注意:' NonCopyable1& NonCopyable1::operator=(const NonCopyable1&) '被隐式删除,因为默认定义是格式错误的: const int NonCopyable1::i,不能使用默认的赋值操作符
完整的例子:
#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;
}
所以,如果一个类是不可复制的,你就不能通过下面的方法获取它的副本作为输出!line 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的行为没有指定,可能是未定义的。
and:
笔记 普通可复制类型的对象是唯一可以使用std::memcpy安全地复制的c++对象,或者使用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));
});
示例程序输出,现在它可以编译和运行。它的工作原理!
你好世界 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”。
Here, we simply copy-construct data right into the memory region occupied by outputData. That's what this "placement new" syntax does for us! It does NOT dynamically allocate memory, like the new operator normally does. Normally, the new operator first dynamically allocates memory on the heap and then constructs an object into that memory by calling the object's constructor. However, placement new does NOT do the allocation part. Instead, it simply skips that part and constructs an object into memory at an address you specify! YOU have to be the one to allocate that memory, either statically or dynamically, beforehand, and YOU have to ensure that memory is properly aligned for that object (see alignof and alignas and the Placement new example here) (it will be in this case since we explicitly created the outputData object as an object, calling it's constructor with NonCopyable1 outputData;), and YOU have to ensure that the memory buffer/pool is large enough to hold the data you are about to construct into it.
泛型放置的新语法是这样的
// 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通过复制构造直接到目标内存位置使用放置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);
// 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. 什么是非平凡可复制对象?
A non-trivially-copyable object may be one which contains virtual methods and things, as this can lead to the class having to track "vee pointers" (vptr) and "vee tables" (vtbls), to point to the proper virtual implementation in memory. Read more about that here: Dr. Dobb's "Storage Layout of Polymorphic Objects". However, even in this case, so long as you're memcpy()ing from the same process to the same process (ie: within the same virtual memory space), and NOT between processes, and NOT deserializing from disk to RAM, it seems to me that memcpy() would technically work fine and produce no bugs (and I've proven this in a handful of examples to myself), but it technically seems to be behavior which is not defined by the C++ standard, so therefore it is undefined behavior, so therefore it can't be relied upon 100% from compiler to compiler, and from one version of C++ to the next, so...it is undefined behavior and you shouldn't memcpy() in that case.
换句话说,如果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: main.cpp:151:13: error: static断言失败:NonCopyable1必须是一个普通可复制的类型,以保证' memcpy() '对其使用是安全的。 static_assert(std::is_trivially_copyable<NonCopyable1>::value, "NonCopyable1必须" ^~~~~~~~~~~~~
所以,你必须/(真的应该)使用“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"预分配缓冲区/内存池的信息
如果你真的只是使用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]);
然后通过outputDataPtr指针读取outputData对象的内容。
The former method (NonCopyable1 outputData;) is best if a constructor for this class exists which requires no input parameters you do NOT have access to at the time of the creation of this buffer, and if you only intend to store this one data type into this buffer, whereas the latter uint8_t buffer method is best if you either A) do NOT have access to all of the input parameters required to even construct the object at the location you need to create this buffer, OR B) if you plan to store multiple data types into this memory pool, perhaps for communicating between threads, modules, processes, etc, in a union-sort of way.
更多关于c++的内容,以及为什么它让我们在这种情况下跳过这些圈
So, this whole "placement new" thing in C++, and the need for it, took me a lot of study and a long time to wrap my mind around it. After thinking about it, it has occurred to me that the paradigm of C (where I come from) is to manually allocate some memory, then stick some stuff into it. These are intended to be separate actions when dealing with both static and dynamic memory allocation (remember: you can't even set default values for structs!). There is no concept of a constructor or destructor, and even getting the behavior of a scope-based destructor which gets automatically called as a variable exits a given scope is a pain-in-the-butt and requires some fancy gcc extension __attribute__((__cleanup__(my_variable))) magic as I demonstrate in my answer here. Arbitrarily copying from one object to another, however, is super easy. Just copy the objects around! This is contrasted to the paradigm of C++, which is RAII (Resource Acquisition is Initialization). This paradigm focuses on objects being ready for use the instant they are created. To accomplish this, they rely on constructors and destructors. This means that creating an object like this: NonCopyable1 data(someRandomData);, doesn't just allocate memory for that object, it also calls the object's constuctor and constructs (places) that object right into that memory. It tries to do multiple things in one. So, in C++, memcpy() and the assignment operator (=; AKA: operator=() function) are explicitly more limited by the nature of C++. This is why we have to go through the hoops of this weird "copy-construct my object into a given memory location via placement new" process in C++ instead of just creating a variable and copying stuff into it later, or memcpy()ing stuff into it later if it contains a const member, like we would do in C. C++ really tries to enforce RAII, and this is in part how they do it.
你可以使用std::optional<>::emplace()代替
从c++ 17开始,你也可以使用std::optional<>作为包装器。现代c++的各种容器和包装器的emplace()函数做的是我们上面手动做的“placement new”(也见我在这里的回答和关于std::vector<T,Allocator>::emplace_back“通常使用place -new来构造元素in-place”的引用)。
optional静态地为你想要放入的对象分配一个足够大的缓冲区。然后,它要么存储该对象,要么存储std::nullopt(与{}相同),这意味着它不存储该对象。要用另一个对象替换其中的一个对象,只需调用std::可选对象上的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()将新数据放入缓冲区。注意,不再需要手动调用析构函数,因为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!");
}
样例输出:
你好世界 (* outputData)。I (after) = 999 outputData.value()。I (after) = 999
在这里运行完整的示例代码。
参考文献和补充,优秀的阅读:
*****+[some of the most-useful and simplest "placement new" examples Iv'e ever seen!] https://www.geeksforgeeks.org/placement-new-operator-cpp/ [great example] https://en.cppreference.com/w/cpp/language/new --> see the "Placement new" section and example here! (I helped write the example). How do I make this C++ object non-copyable? [makes the really important point that calling the placement new line calls the object's constructor as it constructs it!: Line #3 (Fred* f = new(place) Fred();) essentially just calls the constructor Fred::Fred(). This means "the this pointer in the Fred constructor will be equal to place".] http://www.cs.technion.ac.il/users/yechiel/c++-faq/placement-new.html http://www.cs.technion.ac.il/users/yechiel/c++-faq/memory-pools.html Dr. Dobb's "Storage Layout of Polymorphic Objects" [good pre-C++11 intro to the C++ "Rule of Three"] http://web.stanford.edu/class/archive/cs/cs106b/cs106b.1084/cs106l/handouts/170_Copy_Constructor_Assignment_Operator.pdf My "hello world" example and repository, demoing custom copy constructors, assignment operators, etc., related to the C++ "Rule of Three" / "Rule of Five" / "Rule of Zero" / "Rule of 0/3/5": https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/cpp/copy_constructor_and_assignment_operator/copy_constructor_and_assignment_operator.cpp [an excellent writeup by Microsoft on the usage of C++17's std::optional<> type] https://devblogs.microsoft.com/cppblog/stdoptional-how-when-and-why/ [related, since "placement new" very clearly solves this problem too, as this problem was the crux of and driving force behind most of my solutions and examples here!] const member and assignment operator. How to avoid the undefined behavior?
Head Geek: BINGO! You got it totally - that's exactly what it's perfect for. In many embedded environments, external constraints and/or the overall use scenario forces the programmer to separate the allocation of an object from its initialization. Lumped together, C++ calls this "instantiation"; but whenever the constructor's action must be explicitly invoked WITHOUT dynamic or automatic allocation, placement new is the way to do it. It's also the perfect way to locate a global C++ object that is pinned to the address of a hardware component (memory-mapped I/O), or for any static object that, for whatever reason, must reside at a fixed address.
我认为任何答案都没有强调这一点,但新位置的另一个好例子和用法是减少内存碎片(通过使用内存池)。这在嵌入式和高可用性系统中特别有用。在最后一种情况下,这一点特别重要,因为对于一个必须运行24/365天的系统来说,没有碎片是非常重要的。此问题与内存泄漏无关。
Even when a very good malloc implementation is used (or similar memory management function) it's very difficult to deal with fragmentation for a long time. At some point if you don't manage cleverly the memory reservation/release calls you could end up with a lot of small gaps that are difficult to reuse (assign to new reservations). So, one of the solutions that are used in this case is to use a memory pool to allocate before hand the memory for the application objects. After-wards each time you need memory for some object you just use the new placement to create a new object on the already reserved memory.
这样,一旦应用程序启动,就已经预留了所需的所有内存。所有新的内存预留/释放都将分配到已分配的池中(您可能有几个池,每个池对应一个不同的对象类)。在这种情况下不会发生内存碎片,因为没有间隙,您的系统可以运行很长时间(数年)而不会出现内存碎片。
我在实践中看到过这种情况,特别是在VxWorks RTOS中,因为它的默认内存分配系统受到了很多碎片的影响。因此,通过标准的new/malloc方法分配内存在项目中基本上是被禁止的。所有的内存预留都应该到一个专用的内存池中。
它被std::vector<>使用,因为std::vector<>通常分配比vector<>中的对象更多的内存。
一般来说,放置新是为了摆脱“正常新”的分配成本。
我使用它的另一个场景是,我想要访问一个仍待构造的对象的指针,以实现每个文档的单例。