当使用 unique_ptr 作为数据成员时,如何利用编译器生成的复制/移动 ctor?

How to leverage compiler-generated copy/move ctors when using unique_ptr as a data member?

提问人:davidA 提问时间:4/4/2023 更新时间:4/4/2023 访问量:74

问:

假设我有一个包含数据成员的类,并且我希望该类支持复制,方法是深度克隆智能指针指向的对象:unique_ptr

#include <iostream>
#include <memory>

using namespace std;

class Bar {
public:
    Bar() { cout << "Bar::ctor " << this << '\n'; }
    ~Bar() { cout << "~Bar" << this << '\n'; }
    std::unique_ptr<Bar> clone() const { 
        auto p = std::make_unique<Bar>(*this); 
        cout << "Bar::clone() " << this << " to " << p << '\n';
        return p;
    }
};

class Foo {
public:
    Foo() = default;
    ~Foo() = default;
    Foo(Foo const & other) : p_{other.p_->clone()} { 
        cout << "Foo::copy-ctor " << p_ << ", other.p_ " << other.p_ << '\n';
    }
    Foo(Foo && other) : p_{std::move(other.p_)} {
        cout << "Foo::move-ctor " << p_ << ", other.p_ " << other.p_ << '\n';
    }
    Foo & operator=(Foo const & other) { 
        cout << "Foo::copy-assign " << p_ << ", other._p " << other.p_ << '\n';
        p_ = other.p_->clone(); return *this;
    }
    Foo & operator=(Foo && other) { 
        cout << "Foo::move-assign " << p_ << '\n';
        p_ = std::move(other.p_); return *this; 
    }

    void store(std::unique_ptr<Bar> p) {
        cout << "Foo::store() " << p << '\n';
        p_ = std::move(p);
    }

public:  // so we can print out p_ later
    std::unique_ptr<Bar> p_;
};

std::unique_ptr<Bar> bar() {
    return std::make_unique<Bar>();
}

int main() {
    Foo f;

    auto b = bar();
    cout << "b " << b << '\n';

    f.store(std::move(b));

    cout << "copy" << '\n';
    auto g {f};
    cout << "assign" << '\n';
    Foo h;
    h = f;
    cout << "move" << '\n';
    auto i = std::move(f);

    cout << "f.p_ " << f.p_ << '\n';
    cout << "g.p_ " << g.p_ << '\n';
    cout << "h.p_ " << h.p_ << '\n';
}

https://godbolt.org/z/qjf8MoKsE

输出:

Bar::ctor 0x2314eb0
b 0x2314eb0
Foo::store() 0x2314eb0
copy
Bar::clone() 0x2314eb0 to 0x2315ee0
Foo::copy-ctor 0x2315ee0, other.p_ 0x2314eb0
assign
Foo::copy-assign 0, other._p 0x2314eb0
Bar::clone() 0x2314eb0 to 0x2315f00
move
Foo::move-ctor 0x2314eb0, other.p_ 0
f.p_ 0
g.p_ 0x2315ee0
h.p_ 0x2315f00
~Bar0x2314eb0
~Bar0x2315f00
~Bar0x2315ee0

这有点冗长,但关键是由于数据成员及其删除的复制构造函数而不起作用,因此我必须手动定义这些函数才能获得我寻求的深度复制语义。= default;unique_ptr

对于单个数据成员来说,这还不错。但是,如果我向类中添加其他数据成员,在我看来,我将不得不在四个复制/移动 ctors/assignment 运算符中的每一个中显式复制/分配它们,因为尚未生成默认成员函数。unique_ptrFoo

这让我想知道是否有一个标准的库包装器可以实现自动深度复制,以及保持正确性之类的东西,这将使我能够简化为以下内容:unique_ptrconstFoo

class Foo {
public:
    Foo() = default;
    ~Foo() = default;
    Foo(Foo const & other) = default;
    Foo(Foo && other) = default;
    Foo & operator=(Foo const & other) = default;
    Foo & operator=(Foo && other) = default;

    // ...

public:
    DeepCopyUniquePtr p_;  // what is this?
    int a;
    float b; 
    // lots...
    char z;
};

从而回退到默认的复制/移动 ctor 和赋值运算符。

我还发现,当我有多个数据成员时,这些函数也会很快变得混乱。unique_ptr

C++ 复制构造函数 unique-ptr deep-copy

评论

2赞 Sam Varshavchik 4/4/2023
不,C++库中没有这样的“包装器”。但自己写一个没什么大不了的。
1赞 alagner 4/4/2023
长话短说:不,没有,但写自己的装饰应该没有那么难。至于一致性的正确性,你可能会看看propagate_const但同样,它很容易在咖啡准备好时写出来;)
0赞 Daniel Langr 4/4/2023
您可以查找关键字“deep_ptr”、“clone_ptr”或“value_ptr”来获取一些实现。
0赞 Red.Wave 4/4/2023
您仍然可以移动成员。您需要定义的是副本版本。并且杆件实际上不是必需的,除非有特殊的设计考虑。 进行深度复制。然后,您可以复制/移动组合来定义复制分配:;移动-分配复制构造的版本。或者,您可以使用复制/交换惯用语将赋值统一到一个按值赋值中。=defaultclonep_{std::make_unique<Bar>(*other.p_)}return (*this)=Foo{other};
0赞 davidA 4/5/2023
@Red.Wave 好点 - 它实际上是在我的真实代码中作为派生类的宿醉,所以它作为(什么)虚函数存在,以防止复制时切片,何时是指向基类的指针。我很欣赏这没有显示在我的示例代码中,我可能应该删除它。这是你所指的“特殊设计考虑因素”的有效例子吗?cloneBarother.p_p_

答:

4赞 joergbrech 4/4/2023 #1

如评论中所述,您可以编写自己的装饰器。

template <typename Ptr>
struct Clonable
{
    Clonable() = default;

    Clonable(Ptr&& ptr) : p(std::move(ptr)) {}

    Clonable(Clonable const& other)
     : p{other.p->clone()}
    {}
    
    Clonable(Clonable&& other)
     : p{std::move(other.p)}
    {
        other.p = nullptr;
    }

    Clonable& operator=(Clonable const & other)
    {
        p = other.p->clone();
        return *this;
    }

    Clonable& operator=(Clonable&& other){
        p = std::move(other.p);
        other.p = nullptr;
        return *this;
    }

    auto operator*() {
        return *p;
    }

    auto operator*() const {
        return *p;
    }

    auto operator->() {
        return p.operator->();
    }

    auto operator->() const {
        return p.operator->();
    }

    Ptr p;
};

template <typename T>
using DeepCopyUniquePtr = Clonable<std::unique_ptr<T>>;

https://godbolt.org/z/eTqTEncfa

评论

0赞 joergbrech 4/5/2023
如果您有 c++20,您可能希望使用适当的概念来约束指针算术和成员函数。Clonableclone