复制语义和向量

6
我正在处理为内部使用分配内存的对象。目前,它们是不可复制的。
例如:
class MyClass
{
public:
    MyClass() { Store = new int; }
    ~MyClass() { delete Store; }

private:
    int* Store;
};

我想要添加允许赋值的必要条件,包括复制语义和移动语义,并且还能够以移动语义存储它们在向量中。我不想使用智能指针,希望保持简单。

我应该定义哪些类成员?如何强制执行复制赋值或移动赋值?当按值或按引用传递对象时,将执行哪种复制操作?C++的连续版本之间实现会有所不同吗?

例如:

MyClass A, B;
A = B; // How to force copy or move ?
std::vector<MyClass> V = { A, B };

4
观看这个视频来自CppCon 2019。Arthur O'Dwyer在写一个不那么天真的自定义向量类时解决了这个确切的问题。回归基础:RAII和零规则 - tbxfreeware
4
请问为什么您不想使用智能指针呢?它们可以让您的生活变得更轻松,而且不会引入额外的开销。 - chrysante
3
@chrysante:告诉你整个故事,这段代码是传统的遗留代码,完全没有使用stl库,我希望它保持原样。(在代码之外使用std::vector没有矛盾。) - user21508463
3
一个 RAII 的 unique_ptr(与 shared_ptr 不同)很容易自己编写,即使只用于你的 MyClass,它会将所有与内存管理相关的成员函数分离开来。它们一起的代码行数不会比将所有这些放在 MyClass 中多太多。 - Sebastian
2
@n.m.couldbeanAI 我猜这只是一个例子(在原始代码中,它可能比一个简单的int要大得多)。 - Fareanor
显示剩余12条评论
3个回答

1
如果您管理的数据是固定大小的,可以直接将其放在类本身中:
class MyClass
{
    Data data;
};

如果您要处理的数据大小不同,可以使用向量:
class MyClass
{
    std::vector<unsigned char> buffer;
};

再简单不过了。

谢谢,你说得对。目前,将代码更改为在内部使用stl容器不是一个选择。 - user21508463
1
@YvesDaoust 是因为您没有访问标准库吗?如果您有约束,请在问题本身中告诉我们。 - Aykhan Hagverdili
约束条件是从C++03开始的可移植性,并尽量少修改遗留代码。 - user21508463
2
@YvesDaoust 是的,C++03 的 vector 在扩展时会复制所有对象。您可以调用 reserve 来预先分配缓冲区以最小化复制。但是目前 C++03 已有20年的历史。大多数项目已经转向使用 C++11 或更高版本。 - Aykhan Hagverdili
1
@AyxanHaqverdili:我的用户相当保守,有些人可能仍在使用VS2008。但我同意这种情况正在逐渐消失。 - user21508463
显示剩余2条评论

1
你需要遵循“五法则”,需要定义以下内容:
- 析构函数 - 拷贝构造函数 - 移动构造函数 - 拷贝赋值运算符 - 移动赋值运算符
如果使用智能指针,你可以使用`default`关键字来默认实现这些函数,这将是更简洁的解决方案。
但是,由于你不想使用智能指针,你需要按照传统的方式自己完成所有操作。
a = b;            // copy-assignment
a = std::move(b); // move-assignment

std::move()基本上会将参数static_cast为右值引用,以便调用适当的运算符重载(移动赋值)。

"通过值或引用传递对象时,会执行哪种复制操作?"

通过值传递时,会进行复制操作。如果想要移动而不是复制,需要再次使用std::move()

通过引用传递时,不会进行复制或移动操作。可以将其视为传递对象的指针(除了引用不可为空之外)。

"C++的连续版本中,实现是否会有所不同?"

谁知道未来呢?但应该不会。通常他们倾向于尽可能保持向后兼容,以避免破坏旧代码。但有时候也会发生(例如就是一次革命,无法避免地破坏了一些东西,但这是必要的)。
无论如何,我非常怀疑五个规则在将来的版本中会变得错误 :)
一个可能的实现(示例):
class Foo
{
    private:
        int * store;

    public:
        //Default constructor
        Foo() : store{nullptr}
        {}

        // Destructor
        ~Foo()
        {
            delete store;
        }

        // Copy constructor
        Foo(const Foo & f) : store{f.store ? new int{*f.store} : nullptr}
        {}
        // Move constructor
        Foo(Foo && f) noexcept : store{std::exchange(f.store, nullptr)}
        {}

        // Copy-assignment operator
        Foo & operator=(const Foo & f)
        {
            if(!f.store)
            {
                delete store;
                store = nullptr;
                return *this;
            }

            if(!store)
                store = new int{};
                
            *store = *f.store;
            return *this;
        }
        // Move-assignment operator
        Foo & operator=(Foo && f) noexcept
        {
            using std::swap;
            swap(store, f.store);
            return *this;
        }
};

实时示例


1
复制赋值应该只是 *store = *f.store; - M.M
1
移动构造函数和移动赋值运算符是不正确的,因为它们会使源对象处于无效状态(根据拷贝构造函数的设计,store 永远不应该为 null)。 - M.M
1
你的拷贝构造函数和拷贝赋值运算符可能会解引用空指针。像这样管理资源是很困难的,你应该始终使用STL容器和智能指针来代替。 - Aykhan Hagverdili
2
@YvesDaoust 在移动操作之后,对象应该处于“有效但未指定”的状态。因此,在移动之后,用户应该能够设置新值或销毁旧对象。在移动构造函数/移动赋值运算符中进行分配并不是一个好主意,也不是大多数用户所期望的。在这种情况下,你可能干脆不实现移动构造函数。 - Aykhan Hagverdili
2
@Fareanor 这个例子对我来说看起来很好(也许可以添加一个接受 int 的构造函数,以便有一种获取非空对象的方法。对于这个例子来说并不关键)。如果没有其他问题的话,这里的讨论表明了要正确使用这些东西是多么困难,大多数人在大部分时候应该使用 STL 容器。 - Aykhan Hagverdili
显示剩余11条评论

0
你问到了关于向量的问题,那么这里是类 tbx::Vector。我把它写成了一个练习。它的设计遵循了Arthur O'Dwyer在 CppCon 2019 上的演讲。可以看一下标题为 回到基础:RAII和零规则的 YouTube 视频。
为了更加专注于你所问的复制语义,我只包括了五个“特殊”的成员函数,还有几个构造函数以及成员函数reserveswap。尽管类 Vector 实现了几乎所有 std::vector 的接口,但其他函数在这里没有显示出来。
我留下了一些钩子,在运算符new尝试分配失败时,你可以插入所需的代码。
这并不是用来进行生产的代码。首先,为什么要重复造轮子呢?其次,有很多优化被省略了。第三,某些功能被省略了,例如,我没有提供完整的构造函数或对分配器的支持。
template< typename T >
class Vector
{
public:
    using value_type = T;
    using size_type = std::size_t;
    using iterator = value_type*;
    using const_iterator = value_type const*;

private:
    value_type* data_{ nullptr };
    size_type capacity_{};
    size_type size_{};
    enum : size_type { zero, one };

public:
    Vector() noexcept
        = default;

    explicit Vector(size_type const size)
        : data_{ nullptr }, capacity_{ size }, size_{ size }
    {
        if (zero < size)
        {
            try { data_ = new value_type[size]; }
            catch (std::bad_alloc const&) { throw; }
        }
    }

    Vector (Vector const& that)
        : data_{ nullptr }
        , capacity_{ that.size_ }  // "shrink to fit"
        , size_{ that.size_ }
    {
        if (that.size_ != zero)
        {
            try { data_ = new value_type[that.size_]; }
            catch (std::bad_alloc const&) { throw; }
            std::copy(that.data_, that.data_ + size_, data_);
        }
    }

    Vector (Vector&& that) noexcept
        : data_     { std::exchange(that.data_, nullptr)  }
        , capacity_ { std::exchange(that.capacity_, zero) }
        , size_     { std::exchange(that.size_, zero)     }
    {}

    ~Vector() {
        delete[] data_;
    }

    Vector& operator=(Vector that) noexcept {
        swap(that);
        return *this;
    }

    void reserve(size_type const capacity) {
        if (capacity_ < capacity) {
            value_type* p{ nullptr };
            try { p = new value_type[capacity]; }
            catch (std::bad_alloc const&) { throw; }
            if (data_ != nullptr)
                std::copy(data_, data_ + size_, p);
            capacity_ = capacity;
            std::swap(data_, p);
            delete[] p;
        }
    }

    void swap(Vector& that) noexcept {
        using std::swap;
        swap(capacity_, that.capacity_);
        swap(size_, that.size_);
        swap(data_, that.data_);
    }

    friend void swap(Vector& a, Vector& b) noexcept {
        a.swap(b);
    }
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接