“placement new” 有哪些用途?

515

这里有人使用过C++的“定位new”吗?如果使用过,是用来做什么的?在我看来,它似乎只对内存映射硬件有用。


21
这正是我一直在寻找的信息,可以在boost分配的内存池上调用对象构造函数。(希望这些关键词能够方便将来的某个人查找到它)。 - Sideshow Bob
3
它被用于C++11维基百科文章中,作为一个联合体的构造函数。 - HelloGoodbye
1
@HelloGoodbye,很有趣!在您所链接的文章中,为什么不能只做 p = pt 并使用 Point 的赋值运算符,而要做 new(&p) Point(pt)?我想知道两者之间的区别。前者会调用 Pointoperator=,而后者会调用 Point 的复制构造函数吗?但我还不是很清楚为什么一个比另一个更好。 - Andrei-Niculae Petre
1
@Andrei-NiculaePetre 我自己没有使用过放置 new,但我猜想如果你当前没有该类的对象,你应该使用它——连同复制构造函数一起使用,否则你应该使用复制赋值运算符。除非该类是平凡的;否则无论你使用哪个都无所谓。同样的事情也适用于对象的销毁。如果对于非平凡的类不能正确处理这个问题,很可能会导致奇怪的行为,甚至在某些情况下可能会导致未定义的行为。 - HelloGoodbye
1
这里有一些优秀的使用演示/示例,以说明如何使用放置new并展示其功能!https://www.geeksforgeeks.org/placement-new-operator-cpp/ - Gabriel Staples
显示剩余6条评论
25个回答

3
请参阅 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;

3

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


1

我也有一个想法。 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");
    }
}
  • 使用简单的malloc
  • 以C方式检查是否失败
  • 如果成功,我们使用定位new
  • 手动调用析构函数(我们不能只调用delete)
  • 调用free,因为我们调用了malloc

更新 @Useless 写了一条评论,向我展示了new(nothrow)的存在,这应该在这种情况下使用,而不是我之前写的方法。请不要使用我之前写的代码。抱歉。


1
你肯定可以使用new(nothrow)吧? - Useless
@useless,实际上你说得对。我甚至不知道nothrow的存在。看来,我们可以把我的答案扔进垃圾桶了。你觉得我应该删除这个答案吗? - CPPCPPCPPCPPCPPCPPCPPCPPCPPCPP
1
它仍然是正确的,所以我不认为有必要将其删除。留下它和评论也没有任何损害。 - Useless

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会给出以下输出

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

-1
这里有人用过C++的“放置new”吗?如果使用过,它是用来做什么的?在我看来,它似乎只在内存映射硬件上有用。
当需要复制输出以下内容时,它非常有用:
1. 非可复制对象(例如:operator=()已被自动删除,因为类包含const成员),或者
2. 非平凡可复制对象(使用memcpy()是未定义行为)。
这些(从函数中获取这些不可复制或非平凡可复制对象)可以帮助单元测试该函数,通过允许您查看某个数据对象在经过该函数处理后现在的样子,或者它只是您正常API的一部分,以满足您的任何需求。 让我们回顾这些示例,并详细说明我的意思和如何使用“放置new”解决这些问题。
TLDR;(太长不看):注意:我测试了此答案中的每一行代码。它有效。它不违反C++标准。

Placement new 是:

  1. 在 C++ 中,当 operator=()(赋值运算符)被删除时,它是用来替代 = 的,你需要将一个因此无法复制的对象“复制”(实际上是复制构造函数)到给定的内存位置。
  2. 在 C++ 中,当你的对象不是平凡可复制时,它是用来替代 memcpy() 的,这意味着使用 memcpy() 来复制这个非平凡可复制的对象"可能是未定义的"
重要提示:一个“不可复制”的对象并非真正不可复制。它只是不能通过等号操作符进行复制,这个等号操作符实际上是调用了类的底层operator=()重载函数。这意味着当你执行B = C时,实际上发生的是对B.operator=(C)的调用,而当你执行A = B = C时,实际上发生的是A.operator=(B.operator=(C))。因此,“不可复制”的对象只能通过其他方式进行复制,例如通过类的复制构造函数,因为该类没有operator=()方法。可以使用“放置new”来调用类中可能存在的任何一个构造函数,以便将对象构造到所需的预分配内存位置中。由于“放置new”语法允许调用类中的任何构造函数,因此包括传递现有类实例以使放置new调用类的复制构造函数,从传入的对象中复制构造一个新对象到内存中的另一个位置。将一个对象复制到另一个内存位置...就是一次复制。这个动作创建了原始对象的副本。完成后,您可以拥有两个字节完全相同(根据您的复制构造函数的实现)的对象(实例),位于内存中的两个不同位置。这就是一次复制,只是没有使用类的operator=()方法而已。
因此,如果一个类没有operator=()方法,但它仍然可以使用其复制构造函数放置new语法进行复制,根据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的行为未指定,可能是未定义的

并且:

注释
只有那些不是潜在重叠子对象的平凡可复制类型的对象才能安全地使用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++替代方案是使用“放置new”。

在这里,我们只是将data的副本简单地构造到由outputData占用的内存区域中。这就是这个“放置new”语法为我们做的事情!它不像new运算符通常所做的那样动态分配内存。通常,new运算符首先在堆上动态分配内存,然后通过调用对象的构造函数在该内存中构造一个对象。然而,放置new不执行分配部分。相反,它简单地跳过该部分并在您指定的地址处构造一个对象!您必须是那个为该内存分配内存的人,无论是静态地还是动态地,并且您必须确保该内存适当地对齐以容纳该对象(请参见alignofalignas以及此处的放置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);

            // ...
        });

2. 什么是不可平凡复制对象?

一个非平凡可复制对象可能包含虚方法和其他东西,这可能导致类必须跟踪“vee指针”(vptr)和“vee表”(vtbl),以便在内存中指向适当的虚拟实现。在这里阅读更多信息:Dr. Dobb's "Storage Layout of Polymorphic Objects"。然而,即使在这种情况下,只要您从同一进程memcpy()到同一进程(即:在同一虚拟内存空间内),并且不是在进程之间进行,也不是从磁盘反序列化到RAM,我认为memcpy()技术上可以正常工作并且不会产生错误(我已经在一些示例中证明了这一点),但它在技术上似乎是C++标准未定义的行为,因此它是未定义的行为,因此不能从编译器到编译器,从一个C++版本到另一个版本100%可靠,所以...在这种情况下,它是未定义的行为,您不应该使用memcpy()
换句话说,如果上面的`static_assert(std::is_trivially_copyable::value);`检查失败了,就不要使用`memcpy()`。你必须使用"placement new"代替!
让这个静态断言失败的一种方法是在你的`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 "
             ^~~~~~~~~~~~~

所以,你必须(真的应该)使用“放置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”使用的内容

如果您只是要使用放置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++以及为什么在这种情况下它让我们跳过这些障碍

所以,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

在此处运行完整示例代码

参考资料和额外的,优秀的阅读材料:

  1. *****+[这是我见过的一些最有用和最简单的“放置new”示例!] https://www.geeksforgeeks.org/placement-new-operator-cpp/
  2. [很好的例子] https://en.cppreference.com/w/cpp/language/new --> 在这里查看“放置new”部分和示例!(我帮助编写了示例)。
  3. 如何使此C++对象不可复制?
  4. [重要的一点是,调用放置new行会在构造对象时调用对象的构造函数!:第3行(Fred* f = new(place) Fred();)实际上只是调用构造函数Fred::Fred()。这意味着“Fred构造函数中的this指针将等于place”。] 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. Dr. Dobb's“多态对象的存储布局”
  6. [关于C++“三大法则”的很好的C++11之前的介绍] 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<>类型的优秀写作] https://devblogs.microsoft.com/cppblog/stdoptional-how-when-and-why/
  9. [相关,因为“放置new”非常清楚地解决了这个问题,而这个问题是我大部分解决方案和示例的核心和驱动力!] const成员和赋值运算符。如何避免未定义的行为?

3
答案完全误导了。无论是否可以轻松复制,对象都会通过赋值运算符进行复制,即只需要使用a=b。如果一个类被设为不可复制,那么一定有原因,你不应该尝试复制它。放置新对象与这两种情况都无关。 - Eugene
2
emplace() 用于在容器中构造对象,而不是复制它们!是的,它允许避免不必要的复制,包括不可能的复制。无论是否使用 emplace(),容器始终使用放置 new,这是一个重要的用途 - 如其他答案中所提到的。 - Eugene
我在答案的 TLDR 部分添加了几个非常重要的注释。 - Gabriel Staples
@GabrielStaples:正如你所说,C程序的常规范式是通过某种方式获取一块存储空间,并将数据写入其中,就好像它是所需类型的对象一样。如果C++是作为C的向上兼容变体创建的,那么当它被创建时,该范式似乎可以用于C++中与C中可用的相同类型的对象(尽管不一定适用于仅存在于C++中的类型)。如果这种范式今天不再可用,那就意味着在某个时刻委员会对语言进行了重大的破坏性改变。 - supercat
@GabrielStaples:我的怀疑是,这种突破性变化发生在态度转变之间:“标准没有必要明确处理这些情况,因为每个人都知道它们应该如何工作”,到“标准未强制支持这些结构的失败应被解释为其已被弃用”。 - supercat
显示剩余10条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接