c++c++14shared-ptrraiiownership-semantics

Initialization and management of a shared_ptr to base interface


I have some questions related to the use of a shared_ptr pointing to a base class. Their answers influence each other, and for all three I need the same code snippet to set the context in as minimal a way as possible, like this (all the questions relate to Foo and its held_object_):

#include <memory>
#include <utility>

class Bar  // interface for Scene (see linked question)
{
public:
  virtual ~Bar() = 0;
  // more virtual methods to work with Scene
};

Bar::~Bar() = default;

class Baz : public Bar // interface for Foo
{
public:
  virtual ~Baz() = 0;
  // more virtual methods to work with Foo
};

Baz::~Baz() = default;

class ConcreteBaz : public Baz // actual thing I'm acquiring and releasing
{
// overrides and whatnot
};

class Foo
{
public:
  void acquire(const std::shared_ptr<Baz>& object) {};
  void release(/* actually takes a place to release into */) {};

private:
  std::shared_ptr<Baz> held_object_;
};

int main()
{  
  auto foo = std::make_unique<Foo>();
  auto p_foo = foo.get();

  while (/* condition */)
  {
    auto cbaz = std::make_shared<ConcreteBaz>();
    // Scene gets a copy here

    p_foo->acquire(cbaz);
    // do physical things to cbaz
    p_foo->release(/* actually takes a place to release into */);

    // do other physical things, then acquire cbaz again

    p_foo->acquire(cbaz);
    // do physical things to cbaz
    p_foo->release(/* actually takes a place to release into */);
  }
}

As you can see cbaz is a pointer to a polymorphic hierarchy that can work with both Scene and Foo. ConcreteBaz actually represents a physical entity which a Foo can, as stated in the code, take ownership of (it's a shared_ptr because both Scene and Foo own it, as per this. I removed details of Scene here because it's OT).

Questions

1) How do I initialize held_object_? Can I do it with a single allocation?

In reality, foo doesn't own cbaz until it (physically) gets it, therefore the constructor should initialize the pointer to nullptr. Initially I wanted to use make_shared as per this but I read here that it value-initializes, meaning if I were to do

Foo::Foo()
    : held_object_ {std::make_shared<Baz>()} // equivalent to std::make_shared<Baz>(nullptr)
{
}

the compiler would complain with error: invalid new-expression of abstract class type. This would thus become a duplicate of this question but the accepted answer either leaves the shared_ptr uninitialized or creates a further unique_ptr to... basically it's unclear how I would apply it here, and I don't think I can call reset in the constructor (?).

Should I instead be explicit and say

Foo::Foo()
    : held_object_ {std::shared_ptr<Baz> {nullptr}}
{
}

without caring for the double allocation which make_shared avoids? At this point wouldn't it be almost exactly identical to just : held_object_ {} (move ctor vs. default ctor aside)? EDIT: Can I do it with a single allocation as well, like make_shared does?

2) How do I manage held_object_?

Right now to get ownership of cbaz after calling acquire I eventually get into a call for method

void Foo::setHeldObject_(std::shared_ptr<Baz> object)
{
  this->held_object_ = std::move(object);
}

however in release I will have to destroy the owning shared_ptr (to then set it again come next loop) and make the state of my Foo instance coherent with its physical state IRL. This had me thinking of using std::shared_ptr::reset (before even reading the previous related answer) because I would replace the pointee with nullptr if I called setHeldObject() and set cbaz otherwise. However I can't figure the correct syntax to the method's body as:

EDIT: I believe it should be possible to do it with the same method call, giving or not giving the argument. Would that be possible with reset?

3) There are really only two owners

The main function I wrote here is actually a Process class' run method, but having it defined like this makes it so the use_count bumps to 3 by the time Scene and Foo have their own shared_ptr. From when I make_shared onwards, I'm inside a loop, moreover the logic says there should be only two owners. Is this a use case for weak_ptr where it could make sense to do something like:

int main()
{  
  auto foo = std::make_unique<Foo>();
  auto p_foo = foo.get();

  while (/* condition */)
  {
    auto cbaz = std::make_shared<ConcreteBaz>();
    auto p_cbaz = std::weak_ptr<ConcreteBaz> {cbaz};
    // Scene gets a && via std::move here

    p_foo->acquire(p_cbaz.lock()); // this instantly gets converted to a shared_ptr EDIT: BUT IT DOESN'T INCREASE THE USE COUNT
    // do physical things to cbaz
    p_foo->release(/* actually takes a place to release into */);

    // do other physical things, then acquire cbaz again

    p_foo->acquire(p_cbaz.lock()); // this instantly gets converted to a shared_ptr and would be nullptr had I moved in the first acquire call EDIT: BUT IT DOESN'T INCREASE THE USE COUNT
    // do physical things to cbaz
    p_foo->release(/* actually takes a place to release into */);
  }
}

or is there a better way?


Solution

  • How do I initialize held_object_

    If you want held_object_ to be nullptr then you don't have to do anything. The compiler generated default constructor of Foo will call held_object_s default constructor leaving it as a null pointer.

    How do I manage held_object_

    When you need to release the ownership of the pointer you just call reset without any parameters. That effectively does shared_ptr().swap(*this); which leaves you with a null pointer.