我有一个
Child
类,它保存对外部管理内存的引用(在这种特殊情况下,Vulkan 对象是通过 Vulkan 调用创建/删除的,而不是通过标准 C++ 内存管理生命周期)。为了再现的目的,在下面的示例中通过手动分配内存来举例说明。我仍然想依靠 RAII 来处理我的 C++ 对象并在 Child
的析构函数上释放外部分配的内存。在我的实际代码中,Child
类中有多个Parent
实例,因此我也将其包含在此处以完成。这是 MRE:
#include <cstdlib>
#include <memory>
#include <iostream>
class Child {
public:
Child(void * memoryRegion): mMemoryRegion(memoryRegion){}
~Child(){
std::cout << "Destroying" << std::endl;
free(mMemoryRegion);
}
Child(const Child&) = delete;
Child(Child&&) = default;
Child& operator=(const Child&) = delete;
Child& operator=(Child&&) = default;
private:
void * mMemoryRegion;
};
Child createChild(){
void * memory = malloc(100);
return Child(memory);
}
class Parent {
public:
Parent (Child &c1) : child1(std::move(c1)){}
private:
Child child1;
};
std::unique_ptr<Parent> createParent(){
Child c1 = createChild();
return std::make_unique<Parent>(c1);
}
int main(){
auto parent = createParent();
return 0;
}
如您所见,我尝试禁用复制构造函数,并使用
std::move
语义,但这仍然会导致崩溃:
Destroying
Destroying
free(): double free detected in tcache 2
[1] 3568506 IOT instruction ./main
我确信这是我的移动语义上的一个愚蠢的错误,但我一直无法弄清楚。这样做的正确方法是什么?我可以尝试使用
std::unique_ptr
,我很确定这可以解决问题,但我仍然很好奇是否可以在没有它的情况下解决这个问题。
~Child(){
std::cout << "Destroying" << std::endl;
free(mMemoryRegion);
}
如果这个对象在某个时刻仍然被移动,它的析构函数仍然会执行。
因此,当这个对象从 before all 移动并说完时,两个对象将被销毁,并且将有两个析构函数都尝试
free
同一个指针。再见。
移动语义不要改变核心C++基本原则:构造的每个对象都将被破坏(在格式良好的C++程序中,涉及
exit()
的各种边缘情况对于本次讨论的目的来说并不重要)。
一次移动只是一次“高效”的复制。就这样。当对象移动到类的新实例时,您无法“避免”创建“额外副本”。 C++ 不是这样工作的。您现在有了一个新对象。移出的对象仍然存在。它的析构函数仍然会在某个时刻执行。
您需要考虑到这一点来设计您的课程。
您不能在此处使用默认的移动构造函数。您的移动构造函数必须复制移出对象的指针,就像默认构造函数一样,但然后将移出对象的指针设置为
nullptr
。
您的移动赋值运算符必须
free
移至对象的现有指针,如果其不为空,则复制移出对象的指针,然后将其设置为 nullptr
。
你的析构函数应该调整为
free
只是一个非 nullptr
指针,迂腐。