这里有人使用过C++的“定位new”吗?如果使用过,是用来做什么的?在我看来,它似乎只对内存映射硬件有用。
这里有人使用过C++的“定位new”吗?如果使用过,是用来做什么的?在我看来,它似乎只对内存映射硬件有用。
typedef struct _FP
{
unsigned short int rows;
unsigned short int columns;
double array[1]; /* Actually, array[rows][columns] */
} FP;
C++ 中的就地构造函数的杀手级用途是将其对齐到缓存行以及其他 2 的幂边界。 这里是我的超快指针对齐算法,可以在 5 个或更少的单周期指令中将其对齐到任何 2 的幂边界:my ultra-fast pointer alignment algorithm to any power of 2 boundaries with 5 or less single-cycle instructions:
/* 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");
}
}
我们在这里分配了一个大的结构体,并检查分配是否成功,然后删除它。
但是如果我们关闭了异常处理,就无法使用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");
}
}
更新 @Useless 写了一条评论,向我展示了new(nothrow)的存在,这应该在这种情况下使用,而不是我之前写的方法。请不要使用我之前写的代码。抱歉。
nothrow
的存在。看来,我们可以把我的答案扔进垃圾桶了。你觉得我应该删除这个答案吗? - CPPCPPCPPCPPCPPCPPCPPCPPCPPCPP我有一个想法(适用于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++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
在这种情况下是对齐的。
max_align_t: 16
a: 0x7ffe56b57000
ptr: 0x2416000
Placement new 是:
operator=()
(赋值运算符)被删除时,它是用来替代 =
的,你需要将一个因此无法复制的对象“复制”(实际上是复制构造函数)到给定的内存位置。memcpy()
的,这意味着使用 memcpy()
来复制这个非平凡可复制的对象"可能是未定义的"。operator=()
方法,但它仍然可以使用其复制构造函数和放置new语法进行复制,根据C++标准和C++提供的机制,它仍然可以被定义为“不可复制”的类,并且可以在法律上安全且不产生未定义行为地进行复制,如下所示。=
操作符(operator=()
函数)进行复制。就这样!然而,它仍然可以被合法地复制。请参见上面非常重要的说明。在这里,复制构造是可以的,但是由于我们已经显式删除了赋值运算符,所以复制是被禁止的。尝试执行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的行为未指定,可能是未定义的。
并且:
注释
只有那些不是潜在重叠子对象的平凡可复制类型的对象才能安全地使用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
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++替代方案是使用“放置new”。
data
的副本简单地构造到由outputData
占用的内存区域中。这就是这个“放置new”语法为我们做的事情!它不像new
运算符通常所做的那样动态分配内存。通常,new
运算符首先在堆上动态分配内存,然后通过调用对象的构造函数在该内存中构造一个对象。然而,放置new不执行分配部分。相反,它简单地跳过该部分并在您指定的地址处构造一个对象!您必须是那个为该内存分配内存的人,无论是静态地还是动态地,并且您必须确保该内存适当地对齐以容纳该对象(请参见alignof
和alignas
以及此处的放置new示例)(在这种情况下,它将是这样,因为我们明确地创建了outputData
对象作为一个对象,并使用NonCopyable1 outputData;
调用它的构造函数),并且您必须确保内存缓冲区/池足够大,以容纳您即将构造的数据。
所以,通用的放置 new 语法是这样的:
// 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现在看起来像这样,使用放置new语法代替memcpy()
。请注意,确保对象是平凡可复制的检查不再需要。
===> 最佳C++解决方案--通过直接在目标内存位置上使用放置new进行复制构造来避免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);
// ...
});
memcpy()
到同一进程(即:在同一虚拟内存空间内),并且不是在进程之间进行,也不是从磁盘反序列化到RAM,我认为memcpy()
技术上可以正常工作并且不会产生错误(我已经在一些示例中证明了这一点),但它在技术上似乎是C++标准未定义的行为,因此它是未定义的行为,因此不能从编译器到编译器,从一个C++版本到另一个版本100%可靠,所以...在这种情况下,它是未定义的行为,您不应该使用memcpy()
。// 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 "
^~~~~~~~~~~~~
所以,你必须(真的应该)使用“放置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);
});
如果您只是要使用放置new直接复制构造到内存池/共享内存/预分配对象空间中,那么就没有必要使用NonCopyable1 outputData;
在该内存中构造一个无用的实例,我们之后还需要销毁它。相反,您可以使用字节的内存池。格式如下:
(来自这里的“放置new”部分: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
缓冲区方法最好,如果您要么A)无法访问甚至在需要创建此缓冲区的位置构造对象所需的所有输入参数,或B)如果您计划将多个数据类型存储到此内存池中,例如用于线程、模块、进程之间的通信等,则使用联合方式。
所以,C++中的整个“放置new”和它的必要性,让我花了很长时间去研究和理解。经过思考,我意识到C语言的范式(我的出发点)是手动分配一些内存,然后将一些东西放入其中。这些旨在处理静态和动态内存分配时是分离的操作(记住:你甚至不能为struct
设置默认值!)。没有构造函数或析构函数的概念,即使是获得基于作用域的析构函数的行为,当一个变量退出给定作用域时自动调用也是非常麻烦的,并需要一些花哨的gcc扩展__attribute__((__cleanup__(my_variable)))
魔法正如我在这里的答案中所演示的。然而,从一个对象任意复制到另一个对象却是非常容易的。只需复制对象即可!这与C++的范式形成对比,即RAII(资源获取即初始化)。这种范式侧重于对象在创建时立即准备好使用。为了实现这一点,它们依赖于构造函数和析构函数。这意味着像这样创建一个对象:NonCopyable1 data(someRandomData);
,不仅为该对象分配内存,还会调用对象的构造函数并将(放置)该对象直接放入该内存中。它试图在一个步骤中完成多个任务。因此,在C++中,memcpy()
和赋值运算符(=
;也称为operator=()
函数)受到C++本质上的限制而明显更加有限。这就是为什么我们必须通过在C++中进行这种奇怪的“将我的对象复制到给定的内存位置中的复制构造过程”来代替只是创建一个变量并稍后将东西复制到其中,或者像在C中那样稍后memcpy()
东西进去,如果它包含一个const
成员。C++确实试图强制执行RAII,这在某种程度上就是他们这样做的原因。
std::optional<>::emplace()
从C++17开始,你也可以使用std::optional<>
作为这个的包装器。各种容器和包装器的现代C++ emplace()
函数会自动完成我们手动使用“placement new”所做的工作(请参见我的答案以及关于std::vector<T,Allocator>::emplace_back
“通常使用placement-new在原地构造元素”的引用)。
std::optional
静态分配足够大的缓冲区以容纳要放入其中的对象。然后,它将存储该对象或 std::nullopt
(与 {}
相同),表示它不持有该对象。要用另一个对象替换其中的一个对象,只需在 std::optional
对象上调用 emplace()
方法。这样做会执行以下操作:
就地构造包含的值。如果调用之前
*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;
并将此“放置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);
});
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
Fred* f = new(place) Fred();
)实际上只是调用构造函数Fred::Fred()
。这意味着“Fred
构造函数中的this
指针将等于place
”。] http://www.cs.technion.ac.il/users/yechiel/c++-faq/placement-new.html
std::optional<>
类型的优秀写作] https://devblogs.microsoft.com/cppblog/stdoptional-how-when-and-why/a=b
。如果一个类被设为不可复制,那么一定有原因,你不应该尝试复制它。放置新对象与这两种情况都无关。 - Eugeneemplace()
用于在容器中构造对象,而不是复制它们!是的,它允许避免不必要的复制,包括不可能的复制。无论是否使用 emplace()
,容器始终使用放置 new
,这是一个重要的用途 - 如其他答案中所提到的。 - Eugene
p = pt
并使用Point
的赋值运算符,而要做new(&p) Point(pt)
?我想知道两者之间的区别。前者会调用Point
的operator=
,而后者会调用Point
的复制构造函数吗?但我还不是很清楚为什么一个比另一个更好。 - Andrei-Niculae Petre