复制语义和向量

Copy semantics and vectors

提问人: 提问时间:8/1/2023 更新时间:8/2/2023 访问量:245

问:

我正在处理分配内存供内部使用的对象。目前,它们不可复制。

例如

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 };
c++ 复制构造函数 deep-copy 赋值运算符 c++11

评论

4赞 tbxfreeware 8/1/2023
观看 CppCon 2019 的这段视频。Arthur O'Dwyer 在编写一个不那么幼稚的 roll-your-own 向量类时解决了这个问题。 返璞归真:RAII 和零法
4赞 chrysante 8/1/2023
请问您为什么不想使用智能指针?它们使您的生活更轻松,而不会引入开销。
3赞 8/1/2023
@chrysante:告诉你整个故事,代码是遗留的,根本没有使用,我希望它保持原样。(与使用没有矛盾,这是在代码之外完成的。stlstd::vector
3赞 Sebastian 8/1/2023
RAII(与 不同)很容易自己编写,即使仅用于 you,并且会将内存管理成员功能的所有关注点分开。他们在一起不会采取比把所有这些都放进去更多的线.unique_ptrshared_ptrMyClassMyClass
2赞 Fareanor 8/1/2023
@n.m.couldbeanAI 我怀疑这只是一个例子(在 OP 的真实代码中,它可能比裸露的要大得多int)

答:

1赞 Fareanor 8/1/2023 #1

“我应该定义哪些班级成员?”

您需要遵循五法则,该法则需要定义:

  • 破坏者
  • Copy 构造函数
  • Move 构造函数
  • 复制分配运算符
  • 移动赋值运算符

使用智能指针,您可能可以实现这些功能,基本上就是这样。这将是更简洁的解决方案。default

但是,由于您不想使用智能指针,因此您需要以旧的方式自己完成所有恶作剧。

“如何强制复制分配或移动分配?”

a = b;            // copy-assignment
a = std::move(b); // move-assignment

std::move()基本上将参数转换为 r 值引用,以便调用适当的运算符重载 (move-assignment)。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;
        }
};

Live example

评论

1赞 M.M 8/1/2023
复制分配应该只是*store = *f.store;
1赞 M.M 8/1/2023
move-construct 和 move-assign 不正确,因为它们使源处于无效状态(复制构造函数设计所证明的绝不应为 null)store
1赞 Aykhan Hagverdili 8/1/2023
复制构造函数和复制赋值运算符可能会取消引用 null 指针。像这样管理资源很困难,您应该始终使用 STL 容器和智能指针。
2赞 Aykhan Hagverdili 8/1/2023
@YvesDaoust 移动后,对象应处于“有效但未指定”状态。因此,在移动后,用户应该能够设置新值或销毁旧对象。在移动构造函数/移动赋值运算符中分配不是一个好主意,这当然不是大多数用户所期望的。您最好不要在此时实现移动构造函数。
2赞 Aykhan Hagverdili 8/1/2023
@Fareanor这个例子现在对我来说看起来不错(也许添加一个构造函数,它只需要一个,这样就有一种方法可以得到一个非空对象。对于此示例并不重要)。如果不出意外,这里的讨论表明,要正确处理所有这些问题是多么困难,大多数人应该在大多数时候使用 STL 容器。int
1赞 Aykhan Hagverdili 8/1/2023 #2

如果您管理的数据是固定大小的,则可以将其直接放在类本身中:

class MyClass
{
    Data data;
};

如果要管理的数据大小不同,则可以使用向量:

class MyClass
{
    std::vector<unsigned char> buffer;
};

没有比这更简单的了。

评论

0赞 8/1/2023
谢谢,你是对的。目前,无法选择将代码更改为在内部使用容器。stl
1赞 Aykhan Hagverdili 8/1/2023
@YvesDaoust是因为您无权访问标准库?如果您有限制,请在问题中告诉我们。
0赞 8/1/2023
约束是从 C++ 03 开始的可移植性,并尽可能少地修改遗留代码。
2赞 Aykhan Hagverdili 8/1/2023
@YvesDaoust 是的,C++03 向量在扩展时将复制所有对象。您可以调用以预先分配缓冲区以最大程度地减少副本。但 C++03 此时已经有 20 年的历史了。大多数项目已经转向使用 C++11 或更高版本。reserve
1赞 8/1/2023
@AyxanHaqverdili:我的用户非常保守,有些人可能仍然使用 VS2008。但我同意这往往会消失。
0赞 tbxfreeware 8/1/2023 #3

你问了向量,所以......这是类.我把它写成一个练习。它的设计遵循了Arthur O'Dwyer在CppCon 2019上的演讲。请观看YouTube视频,标题为“回归基础:RAII和零法则”。tbx::Vector

为了更深入地关注你所问的复制语义,我只包括了五个“特殊”成员函数,以及几个其他的 ctor 和成员函数和 .虽然类实现了几乎所有的接口,但其他函数在这里没有出现。reserveswapVectorstd::vector

我留下了钩子,您可以在其中插入所需的任何代码,以防操作员尝试的分配失败。new

这不用作生产代码。首先,为什么要重新发明轮子?第二,有很多优化被省略了,第三,一些功能被省略了,例如,我没有提供完整的 ctor 补充或对分配器的支持。

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);
    }
};