按值放置新的 return 并安全地处理临时副本

Placement new, return by value and safely dispose temporary copies

由于复杂的情况(在前面的问题中解释过)我想return函数X中的一个对象,但是在X间接调用的另一个函数Y中创建它。它们之间调用堆栈中的第 3 方代码不会合作传递对象。 X 只能将指针传递给 Y 并接收返回的指针。

我想出了一个使用 placement new 的解决方案,但主要担心它是否可移植、不会调用任何未定义的行为并安全地处理分配的对象。也欢迎任何避免不必要的副本的改进。这是尽可能精简的完整测试程序:

#include <new>
#include <type_traits>
#include <cstdio>

class A {
public:
    A() {
        printf("Create A @ %p\n", this);
    }

    A(const A &other) {
        printf("Copy A @ %p\n", this);
        printf("From another A %s @ %p\n", other.valid ? "OK" : "NOT OK", &other);
        valid = other.valid;
    }

    A(A &&other) {
        printf("Move A @ %p\n", this);
        printf("From another A %s @ %p\n", other.valid ? "OK" : "NOT OK", &other);
        valid = other.valid;
    }

    ~A() {
        printf("Destroy A %s @ %p\n", 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 @ %p\n", 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 @ %p\n", 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() 被调用时一切正常吗?代码有问题吗?

您可以查看一个 class 来管理像 wrapA 这样的资源,您基本上需要问两个问题:

  1. 它是否正确地管理其资源:正确的构建、分配、销毁。
  2. 它的任何 public 数据或函数是否可能允许轻易破坏资源管理方案?

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

  • class 有一个数据成员代表 space 来保存 A,但不一定是实际的 A。这很好
  • 但是,wrapA 的构造函数不会构造 A,但析构函数会尝试破坏 A。因此,如果您忘记在 wrapA 上调用 init,就会出现未定义的行为。我会改变这个设计;最基本的方法是使用一个布尔标志来跟踪 A 是否已实际构造。
  • 然而,wrapA 将获得自动构造的副本 constructor/assignment(这已被弃用)。这些自动生成的函数不会正确调用 A 的副本 constructor/assignment,因为 wrapA 实际上并不拥有 A,它们只会按位复制 A。所以如果 A 是非平凡的,这些函数将无法正常工作。您应该明确地编写这两个函数,或者 = 删除它们,以便 wrapA 变得不可复制。虽然,wrapA 将既不可复制又不可移动,所以使用
  • 可能很烦人

至于2:

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

简而言之,wrapA 并非完全错误,因为您可以很好地使用它(如您所演示的)。然而,这也不完全正确。它不满足您期望 c++ class 满足的保证,因此我认为使用 wrapA 很容易编写错误代码。我认为如果你解决了关于析构函数和副本的问题,使用起来会安全得多 constructor/assignment.