移动持有 unique_ptr 的类实例

Posted

技术标签:

【中文标题】移动持有 unique_ptr 的类实例【英文标题】:move class instance holding a unique_ptr 【发布时间】:2021-03-22 15:05:58 【问题描述】:

我有一个类实例向量。这些实例中的每一个都有一个指向另一个类的 unique_ptr。 由于我从未尝试复制类实例甚至共享指针,我觉得 unique_ptr 比 shared_ptrs 更合适,因为指针不共享,而只能通过类实例访问。

这是不好的做法吗?为什么这行不通?我知道将实例复制到唯一指针是不正确的,但是由于我移动它,我不明白为什么不允许这样做?

我必须创建一个自定义移动构造函数吗?它应该怎么做?

从向量中删除对象实例后,应该立即删除唯一的 ptr,因为没有任何引用,对吧?

更好理解的代码示例:

class A 
private:
    int number;

public:
    void f() 
        std::cout << "The number is: " << number;
    
    A(int i) : numberi 
    ~A() = default;
;

class B 
    std::unique_ptr<A> good_a;

    B() : good_a std::make_unique<A>(1)  
    ~B() = default;
;

int main()

    std::vector<B> all;

    B my_b(123);

    all.emplace_back(std::move(my_b));


【问题讨论】:

您应该尝试将您的帖子集中在一个问题上。你具体要问什么?如果 B 预计是指向 A 的唯一拥有者,那么 unique_ptr 将是使用的理想指针类型。 还要注意all.emplace_back(std::move(my_b));all.push_back(std::move(my_b));的效果一样。如果你真的想用emplace_back,试试all.emplace_back(123); 请注意,~A() = default; 不是必需的。除非显式声明另一个析构函数,否则对于任何类类型都隐含这一点。 @alterigel 只是它不是。移动构造函数被禁止。 godbolt.org/z/dr8KrsTfq 【参考方案1】:

此答案侧重于您似乎遇到的编译错误。不好或好的做法留给其他人参与。

您的代码有几个错误,但主要错误似乎是您的自定义 B( ) 构造函数禁止默认移动构造函数。如果你添加它,你的代码就会变得格式良好。

这是一个完整的工作代码供参考:

#include <memory>
#include <vector>

class A 
private:
    int number;

public:
    void f();
    A(int i) : numberi 
    ~A() = default;
;

struct B 
    std::unique_ptr<A> good_a;

    B(int k) : good_a std::make_unique<A>(k)  

    B(B&& ) = default;
    B& operator=(B&& ) = default; // not needed for the example, but good for completeness
;

int main()

    std::vector<B> all;
    B my_b(123);
    all.emplace_back(std::move(my_b));

【讨论】:

你说它有几个错误,你介意指出来吗?几个关键词就够了!我可以自己阅读它们,只需要一些关于问题所在的指导 @Troganda 尝试将您的示例放入任何在线编译器中,会突出显示几个错误。 哦,好吧,所以您实际上是在谈论编译错误,而不仅仅是不良做法/作品,但不应该使用错误类型 @Troganda 首先解决编译错误,其次担心不良做法【参考方案2】:

为什么这不起作用?

你所描述的可以工作。

我不明白为什么不允许这样做?

你所描述的都是允许的。

我必须创建一个自定义移动构造函数吗?

不,这不是必需的,除非您定义其他特殊成员函数,或者有其他成员(除了唯一指针)已删除或私有移动构造函数。

从向量中删除对象实例后,应该立即删除唯一的 ptr,因为没有任何引用,对吧?

当超级对象被销毁时,成员被销毁。唯一指针的析构函数在其拥有的指针上调用删除器。

是否有对指向对象的引用对是否被删除没有影响。任何引用已删除对象的内容都将悬空。

这是不好的做法吗?

您所描述的一般情况不一定有什么特别不好的地方,但这取决于具体的细节。

一个潜在的问题是动态分配在某些情况下可能会很昂贵,并且不必要地使用它会变得不必要地昂贵。因此,您应该有一些理由动态分配指向的对象,而不是直接将它们存储为成员。


您的示例中的错误:

您尝试初始化 B(123),但 B 没有接受整数的构造函数。 您尝试在 B 的成员函数之外初始化 B,但其构造函数和析构函数具有私有访问权限。 您有B 的用户声明析构函数,但没有用户声明移动构造函数或赋值运算符,因此该类不可移动,这是存储在std::vector 中的要求。

这是一个不使用不必要的动态分配的固定版本:

struct A 
    int number;
;

struct B 
    A good_a;
;

B my_b123;
all.push_back(my_b);

【讨论】:

尤其是你的最后一段对我影响很大。我意识到你是对的,没有理由为此开设 2 节课。我现在决定合并课程并一劳永逸地避免 ptr 混乱。尽管@SergeyA 给了我一个漂亮的复制粘贴解决方案,但重新思考我的设计是更好的选择。谢谢! @Troganda • 有时指针正确的做法。我假设您在原始问题中的示例是从现实世界的问题中简化而来的。但是,与 eerorika 的回答一样,我发现处理对象是我的首选,只有当这不是正确的解决方案时,我才会查看 std::unique_ptr(我倾向于回避 std::shared_ptr)。 @Eljay 是的,不是的。它当然被简化了,但最终归结为这一点。并且由于示例类 A 实际上只有 2 个函数,它们只从 B 类调用一次,因此合并它们并不是很多工作。处理对象很好,但我通常会尝试将它们放在智能指针中以避免由于缺少析构函数调用等而导致的错误。但是我该跟谁说话。我还是 C++ 的菜鸟! :))【参考方案3】:

请阅读this answear。

根据显式声明的内容,具有默认实现的各个构造函数被隐式定义或删除。下表描述了规则:

由于您使用了显式定义的析构函数(默认),您已禁用(“未声明”)移动构造函数。

因此,要修复它,您必须显式定义移动构造函数或删除析构函数的定义:https://godbolt.org/z/dr8KrsTfq

【讨论】:

我不相信这是正确的。默认析构函数不会阻止生成编译器提供的移动构造函数。根据this page,= default 强制编译器生成析构函数,这不会阻止编译器提供移动构造函数。这是我们公司演示为什么我们应该默认析构函数和某些构造函数而不是继续使用空体用户定义方法的关键点。 @sweenish ~T() = default; 不是用户提供的,而是用户声明的。这个答案是正确的。 我还是不买。该声明告诉编译器提供自己的版本。这与只提供它的编译器没有什么不同,所以它不应该有所作为。但事实并非如此。 godbolt.org/z/Yr7eGos69 我注释掉了 move ctor 和 move assignment,它仍然编译得很好。 This site也支持我的观点。 我发现 this 支持您的观点,但现在我对真正发生的事情感到困惑。 请注意,您可以在标头中声明构造函数和析构函数,然后将它们定义为在 cpp 文件中实现的默认值。我经常这样做,所以可以使用前向声明。

以上是关于移动持有 unique_ptr 的类实例的主要内容,如果未能解决你的问题,请参考以下文章

std::将 std::unique_ptr 移动到 stl 容器中。 (MSVC 编译器问题)

动态内存管理

如何将 unique_ptr 对象作为参数传递给作为库的类

可克隆的类层次结构和 unique_ptr

无法创建 MoveConstructibles 的地图

对象移动