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?
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 astd::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 withnew
. 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 aunique_ptr
or aboost::value_ptr
to store the function object internal guts. Maybe you'd even write asbo_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.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:
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:and that's very easy to prove the correctness of.