Ðаn Ðаn - 3 years ago 164
C++ Question

Should use unique_ptr to more easily implement "move" semantics?

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?

Answer Source

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);
}
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download