放置新的,按价值返还,并安全处置临时副本

Placement new, return by value and safely dispose temporary copies

本文关键字:安全 副本      更新时间:2023-10-16

由于复杂的情况(在前面的问题中解释过在其他地方构造一个按值返回的对象),我想从函数X按值返回一个对象,但在X间接调用的另一个函数Y中创建它。X只能将指针传递给Y并接收回一个指针。

我已经提出了一个使用placementnew的解决方案,但主要担心它是否是可移植的,不会调用任何未定义的行为,并安全地处理分配的对象。为避免不必要的复制而进行的任何改进也是受欢迎的。这是一个完整的测试程序,它被写得尽可能地最小化:

#include <new>
#include <type_traits>
#include <cstdio>
class A {
public:
    A() {
        printf("Create A @ %pn", this);
    }
    A(const A &other) {
        printf("Copy A @ %pn", this);
        printf("From another A %s @ %pn", other.valid ? "OK" : "NOT OK", &other);
        valid = other.valid;
    }
    A(A &&other) {
        printf("Move A @ %pn", this);
        printf("From another A %s @ %pn", other.valid ? "OK" : "NOT OK", &other);
        valid = other.valid;
    }
    ~A() {
        printf("Destroy A %s @ %pn", valid ? "OK" : "NOT OK", this);
        valid = false;
    }
    void bar() {printf("Hello, World! (A %s @ %p)n", valid ? "OK" : "NOT OK", this);}
    bool valid = true;
};
class WrapA {
public:
    WrapA() {printf("Create wrapper! (A @ %p)n", &data);}
    ~WrapA() {
        printf("Destroy wrapper! (A %s @ %p)n", reinterpret_cast<A *>(&data)->valid ? "OK" : "NOT OK", &data);
        // Manually call destructor for instance created using placement new
        reinterpret_cast<A *>(&data)->~A();
    }
    void init() {
        ::new(&data) A();
    }
    A getA() {
        printf("Wrapper returning A %s @ %pn", reinterpret_cast<A *>(&data)->valid ? "OK" : "NOT OK", &data);
        return(*reinterpret_cast<A *>(&data));
    }
    typename std::aligned_storage<sizeof(A), alignof(A)>::type data;
};
A debug(A data) {
    printf("Wrapper returned A %s @ %pn", data.valid ? "OK" : "NOT OK", &data);
    return(data);
}
A test() {
    WrapA wrapper;
    wrapper.init();
    return(debug(wrapper.getA()));
}
int main(void) {
    test().bar();
    return(0);
}

它打印:

Create wrapper! (A @ 0x7fff1d6a5bde)
Create A @ 0x7fff1d6a5bde
Wrapper returning A OK @ 0x7fff1d6a5bde
Copy A @ 0x7fff1d6a5bdf
From another A OK @ 0x7fff1d6a5bde
Wrapper returned A OK @ 0x7fff1d6a5bdf
Move A @ 0x7fff1d6a5c0f
From another A OK @ 0x7fff1d6a5bdf
Destroy A OK @ 0x7fff1d6a5bdf
Destroy wrapper! (A OK @ 0x7fff1d6a5bde)
Destroy A OK @ 0x7fff1d6a5bde
Hello, World! (A OK @ 0x7fff1d6a5c0f)
Destroy A OK @ 0x7fff1d6a5c0f

输出显示,A通过3个不同的内存地址传递,始终保持有效,并且所有副本似乎都被正确销毁。在本例中,test直接调用init,但在实际情况下,test用指向wrapper变量的指针调用其他对象,最终wrapper.init在其他地方被调用,接收到许多具有复杂生存期的参数。

WrapA::init中创建的对象是否安全地传递给main并在WrapA::~WrapA中适当地处置?呼叫A::bar()时一切正常吗?代码有问题吗?

您可以查看一个管理wrapA等资源的类,基本上需要问两个问题:

  1. 它是否正确地管理其资源:正确的建造、分配和销毁
  2. 它的任何公共数据或功能是否可能导致资源管理计划容易被破坏

让我们从1开始。我看到了一些潜在的问题:

  • 该类有一个数据成员,表示容纳a的空间,但不一定是实际的a。这很好
  • 然而,wrapA的构造函数不构造A,但析构函数确实试图析构函数A。因此,如果您忘记在wrapA上调用init,您将得到未定义的行为。我会改变这个设计;最基本的方法是用一个布尔标志来跟踪a是否真的构造好了
  • 然而,wrapA将自动构造复制构造函数/赋值(这是不推荐的)。这些自动生成的函数不会正确地调用A的复制构造函数/赋值,因为wrapA实际上并不拥有A,它们只会逐位复制A。因此,如果A不是平凡的,那么这些函数将无法正常工作。您应该显式地编写这两个函数,或者=删除它们,这样wrapA就变得不可复制了。尽管如此,wrapA将是不可复制和不可移动的,因此使用它可能会很烦人

对于2:

  • getA函数很好,因为它返回一个副本,所以不提供内部资源的句柄

简而言之,wrapA并不是完全错误的,因为你可以很好地使用它(正如你所展示的)。然而,这也不完全正确。它不能满足您期望的c++类所能满足的保证,因此我认为使用wrapA编写有缺陷的代码会很容易。我认为,如果你解决了析构函数和复制构造函数/赋值的问题,使用它会更安全。