【问题标题】:Should use unique_ptr to more easily implement "move" semantics?应该使用 unique_ptr 来更轻松地实现“移动”语义吗?
【发布时间】:2016-11-28 19:02:43
【问题描述】:

编辑:使FooBar 变得不那么琐碎,直接替换为shared_ptr<> 更加困难。


应该将unique_ptr<> 用作实现移动语义的更简单方法吗?

对于像这样的类

class Foo
{
    int* m_pInts;
    bool usedNew;
    // other members ...

public:
    Foo(size_t num, bool useNew=true) : usedNew(useNew) {
        if (usedNew)
            m_pInts = new int[num];
        else
            m_pInts = static_cast<int*>(calloc(num, sizeof(int)));
    }
    ~Foo() {
        if (usedNew)
            delete[] m_pInts;
        else
            free(m_pInts);
    }

    // no copy, but move
    Foo(const Foo&) = delete;
    Foo& operator=(const Foo&) = delete;
    Foo(Foo&& other) {
        *this = std::move(other);
    }
    Foo& operator=(Foo&& other) {
        m_pInts = other.m_pInts;
        other.m_pInts = nullptr;
        usedNew = other.usedNew;
        return *this;
    }
};

随着数据成员的增加,实现移动变得更加繁琐。但是,可移动数据可以放置在单独的struct 中,其实例由unique_ptr&lt;&gt; 管理。这允许=default 用于移动:

class Bar
{
    struct Data
    {
        int* m_pInts;
        bool usedNew;
        // other members ...
    };
    std::unique_ptr<Data> m_pData = std::make_unique<Data>();

public:
    Bar(size_t num, bool useNew = true) {
        m_pData->usedNew = useNew;
        if (m_pData->usedNew)
            m_pData->usedNew = new int[num];
        else
            m_pData->m_pInts = static_cast<int*>(calloc(num, sizeof(int)));
    }
    ~Bar() {
        if (m_pData->usedNew)
            delete[] m_pData->m_pInts;
        else
            free(m_pData->m_pInts);
    }

    // no copy, but move
    Bar(const Bar&) = delete;
    Bar& operator=(const Bar&) = delete;
    Bar(Bar&& other) = default;
    Bar& operator=(Bar&& other) = default;
};

除了unique_ptr&lt;&gt; 实例的内存总是在堆上,这样的实现还存在什么其他问题?

【问题讨论】:

  • std::unique_ptr 当且仅当资源的所有权是唯一的时才使用。 std::unique_ptr 上的移动语义仅用于转移所有权。 github.com/isocpp/CppCoreGuidelines/blob/master/…
  • 史前的calloc是什么?
  • @Dan,但这一点也不难......
  • @Dan 对不起,我错过了你的文字:你想使用一些移动语义,std::unique_ptr 将仅用于此目的。

标签: c++ c++11 move-semantics unique-ptr


【解决方案1】:

是的。您正在寻找的是零规则(作为三/五规则的 C++11 扩展)。通过让您的数据都知道如何复制和移动自己,外部类不需要编写 any 的特殊成员函数。编写这些特殊成员可能容易出错,因此不必编写它们可以解决很多问题。

所以Foo 会变成:

class Foo
{
    std::unique_ptr<size_t[]>  data;

public:
    Foo(size_t size): data(new size_t[size]) { }
};

这很容易证明其正确性。

【讨论】:

    【解决方案2】:

    这被称为零规则。

    零规则规定大多数类不实现复制/移动分配/构造或销毁。相反,您将其委托给资源处理类。

    5 规则规定,如果您实现 5 种复制/移动分配/ctor 或 dtor 中的任何一种,您应该实现或删除所有 5 种(或者,经过适当考虑,默认它们)。

    在您的情况下,m_pInts 应该是唯一指针,而不是原始内存处理的缓冲区。如果它与某些东西(比如大小)相关联,那么您应该编写一个实现 5 规则的指针和大小结构。或者如果可以接受 3 个指针而不是 2 个指针的开销,则只需使用 std::vector&lt;int&gt;

    部分原因是您停止直接调用newnew 是直接管理资源的 5 规则类型中的实现细节。业务逻辑类不要乱用new。它们既不是新的,也不是删除的。

    unique_ptr 只是一类资源管理类型之一。 std::stringstd::vectorstd::setshared_ptrstd::futurestd::function——大多数 C++ std 类型都符合条件。自己写也是个好主意。但是当你这样做时,你应该从“业务逻辑”中剥离资源代码。

    因此,如果您编写了 std::function&lt;R(Args...)&gt; 克隆,您将使用 unique_ptrboost::value_ptr 来存储函数对象的内部内容。也许你甚至会写一个sbo_value_ptr,它有时存在于堆上,有时存在于本地。

    然后你会用std::function 的“业务逻辑”来包装它,它知道所指向的东西是可调用的等等。

    “业务逻辑”std::function 不会实现复制/移动分配/ctor,也不会实现析构函数。它可能会明确地=default 他们。

    【讨论】:

    • @Dan Making m_pInts a unique_ptr 不是很重要,除非它是一个公共成员,在整个代码库中使用了 100 次,其内容在其中移入移出一种摇摇欲坠的方式,有时它拥有它的内容,有时它不拥有。但如果是这样的话,你可以做的任何事情都不是一件容易的事,甚至是你的Bar 计划。
    【解决方案3】:

    我的建议是分离关注点使用组合

    管理已分配内存的生命周期是智能指针的工作。如何将该内存(或其他资源)返回给运行时是智能指针的删除器所关心的问题。

    一般来说,如果您发现自己在编写移动运算符和移动构造函数,那是因为您没有充分分解问题。

    例子:

    #include <cstring>
    #include <memory>
    
    // a deleter
    //
    struct delete_or_free
    {
        void operator()(int* p) const 
        {
          if (free_) {
            std::free(p);
        }
          else {
            delete [] p;
          }
        }
    
      bool free_;
    };
    
    
    class Foo
    {
      //
      // express our memory ownership in terms of a smart pointer.
      //
      using ptr_type = std::unique_ptr<int[], delete_or_free>;
      ptr_type ptr_;
    
      // other members ...
    
      //
      // some static helpers (reduces clutter in the constructor)
      //
      static auto generate_new(int size) {
        return ptr_type { new int[size], delete_or_free { false } };
      }
    
      static auto generate_calloc(int size) {
        return ptr_type { 
          static_cast<int*>(calloc(size, sizeof(int))),
          delete_or_free { true } 
        };
      }
    
    public:
    
        //
        // our one and only constructor
        //
        Foo(size_t num, bool useNew=true) 
          : ptr_ { useNew ? generate_new(num) : generate_calloc(num) }
        {
        }
    
        // it's good manners to provide a swap, but not necessary.   
        void swap(Foo& other) noexcept {
          ptr_.swap(other.ptr_);
        }
    };
    
    //
    // test
    //
    int main()
    {
      auto a = Foo(100, true);
      auto b = Foo(200, false);
    
      auto c = std::move(a);
      a = std::move(b);
      b = std::move(c);
    
      std::swap(a, b);
    }
    

    【讨论】:

    • @Dan 我猜你只需要在允许编辑遗留代码的地方插入智能指针。我认为您知道带有自定义删除器的智能指针可以处理任何类型的遗留资源的释放?例如文件、套接字、curl 句柄、sql 句柄等?
    猜你喜欢
    • 2013-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-09
    • 1970-01-01
    • 1970-01-01
    • 2011-01-30
    • 2017-11-22
    相关资源
    最近更新 更多