Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 760
Ðаn Avatar asked Feb 04 '26 07:02

Ðаn


2 Answers

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.

like image 57
Yakk - Adam Nevraumont Avatar answered Feb 05 '26 19:02

Yakk - Adam Nevraumont


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.

like image 35
Barry Avatar answered Feb 05 '26 19:02

Barry



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!