【问题标题】:Can copy/move ctors be safely used to implement copy/move assignment operators?可以安全地使用复制/移动 ctor 来实现复制/移动赋值运算符吗?
【发布时间】:2014-03-13 04:41:26
【问题描述】:

我认为下面的代码比copy-and-swap 习惯用法更好。

这样,你可以使用两个宏来封装复制和移动赋值运算符的定义。换句话说,您可以避免在代码中显式定义它们。因此,您可以只将注意力集中在 ctors 和 dtor 上。

这种方法有什么缺点吗?

class A
{
public:
    A() noexcept
        : _buf(new char[128])
    {}

    ~A() noexcept
    {
        if (_buf)
        {
            delete[] _buf;
            _buf = nullptr;
        }
    }

    A(const A& other) noexcept
        : A()
    {
        for (int i = 0; i < 128; ++i)
        {
            _buf[i] = other._buf[i];
        }
    }

    A(A&& other) noexcept
        : _buf(other._buf)
    {
        _buf = nullptr;
    }

    A& operator =(const A& other) noexcept
    {
        if (this != &other)
        {
            this->~A();
            new(this) A(other);
        }

        return *this;
    }

    A& operator =(A&& other) noexcept
    {
        if (this != &other)
        {
            this->~A();
            new(this) A(static_cast<A&&>(other));
        }

        return *this;
    }

private:
    char* _buf;
};

【问题讨论】:

  • 你的默认构造函数noexcept到底是怎样的?而且您的移动构造函数不会移动任何东西。在delete 之前,无需检查nullptr。为什么static_cast&lt;A&amp;&amp;&gt; 而不是std::move
  • _buf(other._buf) and other._buf = nullptr;只是移动操作。
  • 如果复制或移动构造函数可以抛出异常,你就有麻烦了。
  • 如果你真的写了other._buf = nullptr;,它应该是移动操作。正如 Kerrek 上面评论的那样,您的两个赋值运算符都没有提供强大的异常保证,而复制和交换可以。
  • @Praetorian noexcept 并不意味着“此函数中的任何内容都不会引发异常”。这意味着“这个函数永远不会发出异常”。 noexcept 保证是函数接口的特征,很像返回类型。值得注意的是,这不是对该功能如何实现的限制。我在争论语义,我知道你个人理解这一点。但我觉得向那些认为noexcept 是编译器针对const 之类的实现进行验证的程序员明确区分是很重要的。

标签: c++ design-patterns c++11 constructor idioms


【解决方案1】:
class A
{
public:
    A() noexcept
        : _buf(new char[128])
    {}

在上面,如果new char[128] 抛出异常,A() 将调用std::terminate()

    ~A() noexcept
    {
        if (_buf)
        {
            delete[] _buf;
            _buf = nullptr;
        }
    }

在上面,看起来还不错。可以简化为:

    ~A() noexcept
    {
        delete[] _buf;
    }



    A(const A& other) noexcept
        : A()
    {
        for (int i = 0; i < 128; ++i)
        {
            _buf[i] = other._buf[i];
        }
    }

在上面,如果new char[128] 抛出异常,将调用std::terminate()。但其他都很好。

    A(A&& other) noexcept
        : _buf(other._buf)
    {
        _buf = nullptr;
    }

在上面,看起来不错。

    A& operator =(const A& other) noexcept
    {
        if (this != &other)
        {
            this->~A();
            new(this) A(other);
        }

        return *this;
    }

在上面,通常我会说这是危险的。如果new(this) A(other); 抛出怎么办?在这种情况下,它不会,因为如果它尝试这样做,程序将终止。这是否是安全行为取决于应用程序(终止对 Ariane 5 来说效果不佳,但在更普通的应用程序中效果很好)。

    A& operator =(A&& other) noexcept
    {
        if (this != &other)
        {
            this->~A();
            new(this) A(static_cast<A&&>(other));
        }

        return *this;
    }

以上应该可以正常工作。虽然我不确定它是否优于下面的非分支版本,但性能相当。行为差异是下面的版本不是自移动分配的无操作。然而,我相信自移动赋值不一定是空操作,因为其中一个后置条件表明结果值是未指定的(另一个后置条件表明它是指定的,导致不可靠的矛盾)。

    A& operator =(A&& other) noexcept
    {
        delete [] _buf;
        _buf = nullptr;
        _buf = other._buf;
        other._buf = nullptr;
        return *this;
    }

【讨论】:

    【解决方案2】:

    它将在您提供的上下文中正常工作。

    当 A 是多态类并且具有虚拟析构函数时,这种技术将是灾难性的。

    【讨论】:

    • 你能举个例子吗?
    • 想象一下,A 是一个“非最终”数据类,例如,保持形状的画笔和颜色。从 A 派生的 B 类是一种特殊的形状(例如圆形)。然后代码B b(...); 和后来的b = A(...) 旨在为b 刷上颜色,而是调用它的析构函数(因为析构函数是一个虚拟 函数)。更糟糕的是,new(this) A 行会将 B 的虚拟表指针替换为 A 的虚拟表指针,这将导致难以发现的错误。
    【解决方案3】:

    您可以极大地通过将unique_ptr&lt;char[]&gt; 用于_buf 来简化这个类:

    class A
    {
    public:
        static const std::size_t bufsize = 128;
    
        A() noexcept
            : _buf(new char[bufsize])
        {}
    
        A(const A& other) noexcept
            : A()
        {
            copy_from(other);
        }
    
        A(A&& other) noexcept = default;
    
        A& operator =(const A& other) noexcept
        {
            copy_from(other);
            return *this;
        }
    
        A& operator =(A&& other) noexcept = default;
    
    private:
        void copy_from(const A& other) noexcept {
            std::copy_n(other._buf.get(), bufsize, _buf.get());
        }
    
        std::unique_ptr<char[]> _buf;
    };
    

    面对未来的变化,课程更短、更惯用、更安全,因为它避免了“聪明”delete + 放置new。我个人会从A()A(const A&amp;) 中删除noexcept,但是如果您希望程序在分配失败时使用terminate,那是您的选择;)

    如果你的目标只是避免编写赋值运算符——我不怪你,它们太平庸了——你应该设计成Rule of Zero

    class A
    {
    public:
        static const std::size_t bufsize = 128;
    
        A() : _buf(bufsize) {}
    
    private:
        std::vector<char> _buf;
    };
    

    那里 - 所有隐式复制和移动。

    【讨论】:

    • +1 这是执行此操作的正确方法,并且应该首选第二个版本,除非您绝对负担不起vector 将花费您的几个额外字节的存储空间。
    猜你喜欢
    • 2011-06-24
    • 2017-11-21
    • 2020-05-16
    • 2017-04-19
    • 2015-06-23
    • 1970-01-01
    • 2017-01-16
    • 1970-01-01
    相关资源
    最近更新 更多