【问题标题】:Is this a proper application of the rule of five with abstract base class and unique_ptr member?这是对具有抽象基类和 unique_ptr 成员的五法则的正确应用吗?
【发布时间】:2012-09-10 22:11:05
【问题描述】:

尝试解决与抽象基类相关的错误C2248,使用复制/移动 ctors/赋值运算符和 dtor(五规则)的实现,出现了几个问题:

1) 为什么在自动处理 unique_ptr 数据成员时适用主要与 dtor 相关的第 5 条规则? dtor 实现应该留空正确,因为一旦其所有者超出范围,unique_ptrs 会自动销毁?

2) 假设另一个类具有相同类型向量的 std::unique_ptr 类型成员。为了让这个类是可复制的,它必须有一个复制 ctor 和复制赋值运算符来克隆 unique_ptr 数据成员?我已经看到了这个solution,但似乎原始海报只是切换到 shared_ptr 只是为了消除错误而很少考虑所有权管理。这是正确的策略吗?

3) 考虑与上述关于 unique_ptr 向量的问题 2 相同的情况。 dtor 是否应该调用 clear() 向量?

4) Derived1 的赋值运算符不正确。但是基类应该具有复制和移动赋值运算符,因为它具有复制/移动 ctor(4/5 规则)。这些实际上不能在类之外使用,因为它是抽象的,因此不会分配实例。但是我如何利用派生类中的这段代码呢?每个派生类都需要能够移动/复制基本数据成员及其自己的数据成员。我不知道该怎么办。

    #include <algorithm>
#include <memory>
#include <vector>
#include <iostream>

class Base{

public:
    Base() : m_subBases(){};

    /*  copy ctor */
    Base(const Base& other) : m_subBases(){
        *this = other;
    };

    /*  move ctor */
    Base(Base&& other) : m_subBases(){
        *this =std::move( other);
    };

    /*  Move assignment operator*/
    Base& operator=(Base&& other){
        m_subBases = std::move(other.m_subBases);
        return *this;
    };

    /*  Copy assignment operator */
    Base& operator=(const Base& other){
        for(int i = 0; i < other.m_subBases.size(); i++)
            m_subBases.push_back(other.m_subBases[i]->clone());

        return *this;
    };

    /* virtual dtor */
    virtual ~Base(){
        m_subBases.clear();
    };

    /* Used for creating clones of unique_ptrs */
    virtual std::unique_ptr <Base> clone() const= 0;

    /* Do something */
    virtual void execute(float f) = 0;

    //Omitted data member access methods

protected:
    std::vector < std::unique_ptr <Base> > m_subBases;
};

class Derived1 : public Base{

public:
    Derived1() :  Base(){};

    /*  copy ctor */
    Derived1(const Derived1& other) : Base(other){
        *this = other;
    };

    /*  move ctor */
    Derived1(Derived1&& other) : Base(std::move(other)){
        *this = std::move(other);
    };

    /*  Move assignment operator*/
    Derived1& operator=(Derived1&& other){

        //This is redundant when called in the move ctor because
        // of the call to Base(std::move(other))
        m_subBases = std::move(other.m_subBases);

        m_string = other.m_string;
        return *this;
    };

    /*  Copy assignment operator */
    Derived1& operator=( const Derived1& other){

        //This is redundant when called in the copy ctor because
        // of the call to Base(other)
        for(int i = 0; i < other.m_subBases.size(); i++)
            m_subBases.push_back(other.m_subBases[i]->clone());

        m_string = other.m_string;
        return *this;
    };

    /* virtual dtor */
    virtual ~Derived1(){};

    /* Used for creating clones of unique_ptrs */
    virtual std::unique_ptr <Base> clone() const{
        return std::unique_ptr <Base> (new Derived1(*this));
    };

    virtual void execute(float f){
        std::cout << "Derived1 " << f << std::endl; 
    };
protected:

    std::string m_string;
};

【问题讨论】:

  • 看!一堵代码墙和一堵文字墙!
  • 是的,请创建一个简短的示例来说明您的问题,并附上简短的解释。读到这里简直让人不知所措。
  • 是的。请编辑您的问题,使其简短明了。例如,“我希望这篇文章的标题问题准确地反映了问题的性质。” 这样的句子不会添加任何与问题相关的有价值的信息。请从问题中删除所有此类句子。
  • 复制构造函数中的赋值很吓人。
  • 我认为要带回家的重要信息是,当您使用像 unique_ptr 这样的组件时,您不要编写任何复制构造函数、复制赋值运算符和析构函数。隐式定义的版本就可以了。

标签: c++ inheritance polymorphism move-semantics unique-ptr


【解决方案1】:

我想提供一种替代方法。正如@Tony The Lion 已经建议的那样,不是可怕的五规则,而是令人愉快的零规则。我的提议的完整实现已经由几个人编码,@R. Martinho Fernandes's library 中有一个很好的版本,但我会提供一个简化版本。

首先,让我们回顾一下:

零规则:不要编写复制或移动构造函数、复制或移动赋值运算符或析构函数。相反,组合您的组件类处理单一职责并封装相关单个资源的所需行为。

有一个明显的警告:当您设计单一职责类时,您当然必须遵守:

五法则:如果您编写复制或移动构造函数、复制或移动赋值运算符或析构函数中的任何一种,则必须实现所有五种。 (但这条规则需要的“五个”函数实际上是:Destructor、Copy-Const、Move-Const、Assignment 和 Swap。)

让我们开始吧。首先,您的消费者:

struct X;

struct Base
{
    std::vector<value_ptr<X>> v;
};

struct Derived : Base
{
};

注意BaseDerived 都遵守零规则!

我们需要做的就是实现value_ptr。如果指针对象是非多态的,则执行以下操作:

template <typename T>
class value_ptr
{
    T * ptr;
public:
    // Constructors
    constexpr value_ptr()      noexcept : ptr(nullptr) { }
    constexpr value_ptr(T * p) noexcept : ptr(p)       { }

    // Rule of Five begins here:
    ~value_ptr() { ::delete ptr; }
    value_ptr(value_ptr const & rhs) : ptr(rhs.ptr ? ::new T(*rhs.ptr) : nullptr) { }
    value_ptr(value_ptr && rhs) noexcept : ptr(rhs.ptr) { rhs.ptr = nullptr; }
    value_ptr & operator=(value_ptr rhs) { swap(rhs); return *this; }
    void swap(value_ptr & rhs) noexcept { std::swap(rhs.ptr, ptr); }

    // Pointer stuff
    T & operator*() const noexcept { return *ptr; }
    T * operator->() const noexcept { return ptr; }
};

template <typename T, typename ...Args>
value_ptr<T> make_value(Args &&... args)
{
    return value_ptr<T>(::new T(std::forward<Args>(args)...));
}

如果你想要处理多态基类指针的智能指针,我建议你要求你的基类提供一个虚拟的clone()函数,并且你实现一个clone_ptr&lt;T&gt;,它的复制构造函数是这样的:

clone_ptr(clone_ptr const & rhs) : ptr(rhs.ptr ? rhs.ptr->clone() : nullptr) { }

【讨论】:

  • 为什么需要全部实现这五个?对于包含仅移动对象的责任类,自己实现复制操作而将其余操作保留为默认值有什么问题?
  • @Grizzly:如果所有其他函数都可以默认设置,那么您的复制构造函数也可以默认设置。无论您采用哪种方式,如果您确实需要定义这五个中的任何一个,您很可能会进行一些需要在所有这些中处理的手动资源处理。
  • 我提到的是一个类包含一个成员变量的场景,该成员变量是可移动的,但不可复制。这几乎不是手动资源处理。在这种情况下,默认值通常适用于析构函数和移动操作,因为这些操作将由成员处理,但是复制需要特别注意,因为成员没有复制功能,所以取决于你在做什么,你要么深入复制或创建新对象。另一种情况是没有复制或移动功能的成员,默认析构函数仍然可以,但没有别的。
  • @Grizzly:嗯,我相信你可以设计出像你描述的那样的情况,但我想说你几乎总是可以通过使用适当的组件来更优雅地处理这个问题,而不是,比如说,为深度复制角色滥用唯一指针。五法则是一个通用的设计指南,而不是绝对的要求。如果您遵循它,您的代码会更好,但您没有必须
  • @KerrekSB 尝试使用代码。查看此link,VS 2010 不支持 constexpr、noexcept 和可变参数模板。我不认为 constexpr 对于功能来说是必不可少的, make_ 函数也不是,因为可以使用 ctor?但是在这个例子中使用 noexcept 有多大意义呢?
猜你喜欢
  • 1970-01-01
  • 2020-05-29
  • 1970-01-01
  • 1970-01-01
  • 2017-10-08
  • 1970-01-01
  • 1970-01-01
  • 2011-01-08
  • 1970-01-01
相关资源
最近更新 更多