基类和派生类中的复制和交换技术

7

我最近了解到 拷贝并交换,现在正在尝试实现基类和派生类的构造函数。我已经在我的基类和派生类中具有了四个构造函数,但是我不确定如何实现派生类的赋值运算符。

explicit Base(int i) : m_i{i} {}
Base(const Base & other) : m_i{other.m_i}
Base(Base && other) : Base(0) { swap(*this, other); }
Base & operator=(Base other) { swap(*this, other); return *this; }
friend void swap(Base & a, Base & b) noexcept {
    using std::swap;
    swap(a.m_i, b.m_i);
}

explicit Derived(int j) : Base(42), m_j(j) {}
Derived(const Derived & other) : Derived(other.m_j) {}
Derived(Derived && other) : Derived(other.m_j) { swap(*this, other); }
Derived & operator=(Derived other) { /*???*/ }
friend void swap(Derived & a, Derived & b) noexcept {
    using std::swap;
    swap(a.m_j, b.m_j);
}

经验法则:单参数非复制/移动构造函数应该是“显式”的:您真的不希望从intBase进行隐式转换... - Deduplicator
此外,您还需要在Derived中定义swap,除非Base中的那个足够好。 - Deduplicator
@Deduplicator:感谢您的提示,我已编辑了我的问题。 - gartenriese
2个回答

10
考虑尽可能使用= default。如果我们谈论公有继承,你确实需要一个虚拟析构函数。
以下是使用复制/交换风格的Base示例:
class Base
{
    int m_i;
public:
    virtual ~Base() = default;
    Base(const Base& other) = default;
    Base& operator=(Base other) noexcept
    {
        swap(*this, other);
        return *this;
    }
    Base(Base&& other) noexcept
        : Base(0)
    {
        swap(*this, other);
    }

    explicit Base(int i) noexcept
        : m_i{i}
        {}

    friend void swap(Base& a, Base& b) noexcept
    {
        using std::swap;
        swap(a.m_i, b.m_i);
    }
};

与您所拥有的唯一区别是我添加了虚析构函数,并对复制构造函数使用了= default

现在来看Derived

class Derived
    : public Base
{
    int m_j;
public:
    Derived(const Derived& other) = default;
    Derived& operator=(Derived other) noexcept
    {
        swap(*this, other);
        return *this;
    }
    Derived(Derived&& other) noexcept
        : Derived(0)
    {
        swap(*this, other);
    }

    explicit Derived(int j) noexcept
        : Base(42)
        , m_j{j}
        {}

    friend void swap(Derived& a, Derived& b) noexcept
    {
        using std::swap;
        swap(static_cast<Base&>(a), static_cast<Base&>(b));
        swap(a.m_j, b.m_j);
    }
};

我已让编译器隐式处理了析构函数,因为编译器会自动提供一个虚拟析构函数,在这种情况下它会做正确的事情。

同样地,我明确地默认了复制构造函数。这纠正了你版本中忽略复制“Base”的错误。

operator=看起来与Base版本相同。

Derived移动构造函数不需要从other中移动或复制任何内容,因为它将要与other进行交换。

Derivedswap函数必须交换Base部分和Derived部分。


现在考虑不要使用复制/交换惯用语。这可能会更加简单,并且在某些情况下性能更高。

对于Base,您可以对其所有5个特殊成员使用= default

class Base
{
    int m_i;
public:
    virtual ~Base() = default;
    Base(const Base&) = default;
    Base& operator=(const Base&) = default;
    Base(Base&&) = default;
    Base& operator=(Base&&) = default;

    explicit Base(int i) noexcept
        : m_i{i}
        {}

    friend void swap(Base& a, Base& b) noexcept
    {
        using std::swap;
        swap(a.m_i, b.m_i);
    }
};

这里真正需要的只是你的自定义构造函数和swap函数。

Derived更容易实现:

class Derived
    : public Base
{
    int m_j;
public:
    explicit Derived(int j) noexcept
        : Base(42)
        , m_j{j}
        {}

    friend void swap(Derived& a, Derived& b) noexcept
    {
        using std::swap;
        swap(static_cast<Base&>(a), static_cast<Base&>(b));
        swap(a.m_j, b.m_j);
    }
};

所有 5 个特殊成员可以被隐式地设置为默认值!

我们不能在 Base 中将它们默认设置,因为我们需要指定虚构函数,这会阻止移动成员的生成,并且使用带有用户声明的析构函数的复制成员的生成已经过时。但是,由于我们不需要在Derived中声明析构函数,所以我们可以让编译器处理所有内容。

作为复制/交换的一个重要卖点是减少编码量,讽刺的是使用它实际上可能需要比让编译器默认设置特殊成员更多的编码。

当然,如果默认值不能做正确的事情,那么就不要使用它们。我只是说,默认值应该是你的首选,而不是复制/交换。


我再次明确地默认了复制构造函数。这纠正了你版本中忽略复制Base的错误。这是否意味着默认的复制构造函数调用其基类的复制构造函数? - gartenriese
1
@gartenriese: 没错。所有默认的特殊成员将首先迭代基类,然后迭代非静态数据成员,依次对每个执行指定的操作。噢,除了析构函数会按构造函数的相反顺序执行此迭代。 - Howard Hinnant
好的,谢谢!顺便说一下,我不能使用默认值,因为我需要复制一些OpenGL的内容。 - gartenriese
如果您使用默认版本的复制和移动构造函数,为什么需要实现交换函数? - Oleksa
我的意思是通过直接调用。 - Howard Hinnant
显示剩余2条评论

4

对于Derived,您需要与Base完全相同地实现op=

Derived& operator=(Derived other) { swap(*this, other); return *this; }

希望你了解传递值的优点和缺点:

  • 优点:只需要一个函数来处理所有值类型。
  • 缺点:对于xvalues而言,需要进行第二次移动操作,加上prvalues所需的复制操作。

其他需要考虑的问题:

  • 经验法则:单参数的非复制/移动构造函数应该是explicit的:你真的不想将int隐式转换为Base...
  • 你忘记重新实现Derivedswap(交换所有子对象,包括基类和成员)。如果Derived没有添加任何成员,也可以放弃它。

1
@Jon:但我喜欢用拇指敲打;-)(谢谢)。 - Deduplicator
哈哈!C++确实有几个应该遵循的经验法则,如果不遵循就应该受到惩罚! :-) - Jonathan Wakely
但是你说我必须在Derived :: swap内部调用Base :: swap。因此,当我调用Derived的move ctor时,它将首先调用Base的move ctor,后者将调用Base :: swap。当Base的move ctor完成后,它进入Derived的move ctor,后者将调用Derived :: swap。同样,也会调用Base :: swap。因此,Base :: swap会被调用两次?我错了吗? - gartenriese
1
啊,你的派生移动构造函数有问题:它需要便宜地默认初始化对象,然后交换所有内容。(如果默认构造函数或交换不便宜,则需要自定义编码)。这让我想到一个问题:为什么你没有默认构造函数?(也许在ctor(int)中添加一个默认值) - Deduplicator
这取决于你。对于应用于移动构造函数的copy&swap惯用语,因此cheap-init和swap,您只需要一种廉价初始化僵尸对象的方法。 - Deduplicator
显示剩余5条评论

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