Should use unique_ptr to more easily implement “mo

2019-04-15 03:24发布

Edit: made Foo and Bar a little less trivial, and direct replacement with shared_ptr<> more difficult.


Should unique_ptr<> be used as an easier way to implement move semantics?

For a class like

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

Implementing move becomes more tedious as data members are added. However, the moveable data can be placed in a separate struct, an instance of which is managed by unique_ptr<>. This allows =default to be used for move:

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

Other than the memory for the unique_ptr<> instance always being on the heap, what other problems exist with an implementation like this?

3条回答
在下西门庆
2楼-- · 2019-04-15 03:54

This is known as the rule of zero.

The rule of zero states that most classes do not implement copy/move assignment/construction or destruction. Instead, you delegate that to resource handling classes.

The rule of 5 states that if you implement any one of the 5 copy/move assign/ctor or dtor, you should implement or delete all 5 of them (or, after due consideration, default them).

In your case, the m_pInts should be a unique pointer, not a raw memory handled buffer. If it is tied to something (say a size), then you should write a pointer-and-size structure that implements the rule of 5. Or you just use a std::vector<int> if the overhead of 3 pointers instead of 2 is acceptable.

Part of this is that you stop invoking new directly. new is an implementation detail in the rule-of-5 types that manage resources directly. Business logic classes do not mess with new. They neither new, nor delete.

unique_ptr is just one of a category of resource-managing types. std::string, std::vector, std::set, shared_ptr, std::future, std::function -- most C++ std types qualify. Writing your own is also a good idea. But when you do, you should strip the resource code from the "business logic".

So if you wrote a std::function<R(Args...)> clone, you'd either use a unique_ptr or a boost::value_ptr to store the function object internal guts. Maybe you'd even write a sbo_value_ptr that sometimes exists on the heap, and sometimes locally.

Then you'd wrap that with the "business logic" of std::function that understands that the thing being pointed to is invokable and the like.

The "business logic" std::function would not implement copy/move assign/ctor, nor a destructor. It would probably =default them explicitly.

查看更多
闹够了就滚
3楼-- · 2019-04-15 04:02

My advice would be to separate concerns and use composition.

Managing the lifetime of allocated memory is the job of a smart pointer. How to return that memory (or other resource) to the runtime is the concern of the smart pointer's deleter.

In general, if you find yourself writing move operators and move constructors it's because you have not sufficiently decomposed the problem.

Example:

#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);
}
查看更多
Ridiculous、
4楼-- · 2019-04-15 04:14

Yes. What you're looking for is called the Rule of Zero (as the C++11 extension of the Rule of Three/Five). By having your data all know how to copy and move themselves, the outer class doesn't need to write any of the special member functions. Writing those special members can be error-prone, so not having to write them solves a lot of problems.

So Foo would become just:

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

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

and that's very easy to prove the correctness of.

查看更多
登录 后发表回答