Home > Mobile >  Do I always have to use a unique_ptr to express ownership?
Do I always have to use a unique_ptr to express ownership?

Time:10-29

Lets say I have a class A that owns an object of class B.

  • A is responsible for creating and deleting this object of B.
  • The ownership must not be transferred to another class.
  • The object of B will never be reinitialized after an object of A was created.

Normally, as far as I know, in modern C we would use a unique_ptr to express that A is the owner of this object / reference:

// variant 1 (unique pointer)
class A {
  public:
    A(int param) : b(std::make_unique<B>(param)) {}

    // give out a raw pointer, so that others can access and change the object 
    // (without transferring ownership)
    B* getB() {
      return b.get();
    }
  private:
    std::unique_ptr<B> b;
};

Someone suggested that I also may use this approach instead:

// variant 2 (no pointer)
class A {
  public:
    A(int param) : b(B(param)) {}

    // give out a reference, so that others can access and change the object 
    // (without transferring ownership)
    B& getB() {
      return b;
    }
  private:
    B b;
};

As far as I understand the main differences are that in variant 2, the memory is allocated coherently, while in variant 1 the memory for the B object can be allocated anywhere and the pointer must be resolved first in order to find it. Also of course, it makes a difference if users of A's public interface can work with a B* or a B&. But in both cases, I am sure that ownership stays within A as I need it.

Do I always have to use variant 1 (unique_ptrs) to express ownership? What are reasons to use variant 2 over variant 1?

CodePudding user response:

You missed the point of unique_ptr. It was precisely created to allow ownership transfer via a simple assignation. If ownership has never to be transfered it is much simpler to have a subobject by using containment.

Memory allocation does not really matter in C at least for a programmer using an object, because many objects internally use dynamic allocation whatever the duration of the object is. An example is std::string. In most implementations, an automatic std::string object will still use dynamic memory for its underlying character array.


Of course library implementors should care for the way their object use dynamic memory. Small String Optimization was invented in recent implementation of the standard library to avoid dynamic allocation for small strings (thanks to NathanOliver for its comment).

CodePudding user response:

Ownership can be expressed in different ways.

Your B b of variant 2 is the simplest form of ownership. The instance of class A exclusively owns the object stored in b and the ownership cannot be transferred to another object or (member) variable.

std::unique_ptr<B> b expresses an unique - but transferable - ownership over the object managed by std::unique_ptr<B>.

But also std::optional<B>, std::vector<B>, … or std::variant<B,C>, expresses ownership.

Also of course, it makes a difference if users of A's public interface can work with a B* or a B&. But in both cases, I am sure that ownership stays within A as I need it.

You can always create a member function that returns B* no matter if the member is B b, or std::unique_ptr<B> b (or also for std::optional<B>, std::vector<B>, … std::variant<B,C>)

Interestingly this code:

Variant 1

class A {
  public:
    A(int param) : b(B(param)) {}

    // give out a reference, so that others can access and change the object 
    B& getBRef() {
      return b;
    }


    B* getB() {
      return &b;
    }
  private:
    B b;
};

can be less problematic then:

Variant 2

class A {
  public:
    A(int param) : b(std::make_unique<B>(param)) {}

    // give out a raw pointer, so that others can access and change the object 
    // (without transferring ownership)
    B* getB() {
      return b.get();
    }
  private:
    std::unique_ptr<B> b;
};

For the Variant 2 case, a call to another member function of A could potentially invalidate the pointer (if that function e.g. assigned another object to the unique_ptr). While for Variant 1 the validity of the returned pointer (or reference) is given by the lifetime of an instance of A

In any case, you then have to treat B * as non owning raw pointer, and make clear in the documentation, how long that raw pointer is valid.


Which kind of ownership you choose depends on the actual use case. Most of the time you will try to stay with the simplest ownership B b. If that object should be optional you will use std::optional<B>.

If the implementation requires it e.g. if you plan to use PImpl, if the data structure might prevent the use of B b (like in the case of tree- or graph-like structures), or if the owner-ship has to be transferable, you might want to use std::unique_ptr<B>.

  • Related