18
Oct
2021

Smart Pointers (6/6): Final: Custom Deleters, Passing Smart Pointers…

In the final part, we will take a look at custom deleters and pass the smart pointers to functions,

Custom Deleters:

Smart pointers have the default deleter but also a custom deleter can be utilized. It brings about some overhead such as increasing the size. We know the std::unique_ptr size is equal to raw’s pointer size. if a custom deleter is used the size of unique_ptr increases but, it depends on the kind of custom deleters such as no-capture lambdas (stateless), std::function object, etc…

The way of creating safely smart pointers: make_shared and make_unique don’t accept custom deleters.

Here is the example to show overheads and custom deleters using methods:

#include <iostream>
#include <memory>
using namespace std;
class embeddedWorld
{
  public_colon
  embeddedWorld(int p_no): planetNo(p_no) {}

  int getPlanetNo() const
  {
    return planetNo;
  };

  private:
    int planetNo = 0;
};

struct StarKillerBase
{
	//deleter

  void sayHi()
  {
    std::cout << "I'm Dart Vader" << std::endl;
  }

  void operator()(embeddedWorld *e) const
  {
    std::cout << "Starkiller Base is deleting the embeddedWorld planet" << std::endl;
    delete e;
  };

};

int main()
{

  std::unique_ptr<int> unique_ptr(new int(10));

  {

  	//sizeof
    auto noCaption_lambda =[](embeddedWorld *e)
    {
      std::cout << "Hi from lambda deleter" << std::endl;
      delete e;
    };

   	//no-caption lambda (stateless): there is no overhead!
    std::unique_ptr<embeddedWorld, decltype(noCaption_lambda) > u_e0(new embeddedWorld(2), noCaption_lambda);
    cout << "custom deleter with no-caption lambda; sizeof: " << sizeof(decltype(u_e0)) << endl;

    int heavy[100];
   	//caption lambda: statefull
    auto caption_lambda =[heavy](embeddedWorld *e)
    {
      std::cout << "Hi from lambda deleter" << std::endl;
      delete e;
    };

   	//size = embeddedWorld* + heavy
    std::unique_ptr<embeddedWorld, decltype(caption_lambda) > u_e1(new embeddedWorld(2), caption_lambda);
    cout << "custom deleter with caption lambda; sizeof: " << sizeof(decltype(u_e1)) << endl;
  }

  {

  	//custom deleter function
    std::unique_ptr<embeddedWorld, StarKillerBase> u_e1(new embeddedWorld(1), StarKillerBase());	//

    auto deleter = u_e1.get_deleter();
    deleter.sayHi();

    std::shared_ptr<embeddedWorld> s_e1(new embeddedWorld(1), StarKillerBase());	//

  }
}

 


Passing Smart Pointers:

When we pass to smart-pointers as a function parameter we need to know some details. According to C++ Core Guidelines, there are a few rules that we should know.

Firstly, we will realize passing the unique_ptr as a function parameter.

Passing std::unique_ptr:

If we use unique_ptr, probably, we need uniqueness. And uniqueness means at the std::unique_ptr can not be copied as well shared. For establishing uniqueness, the function needs to take std::unique_ptr’s reference or directly its object.

For example:

#include <iostream>
#include "memory"

using namespace std;

class embeddedWorld {
  public:
    embeddedWorld(int p_no): planetNo(p_no) {};

  ~embeddedWorld() {
    cout << "inside embeddedWorld Destructor: planet No: " << planetNo << endl;
  } // for debugging

  int getPlanetNo() const {
    return planetNo;
  };

  private:
    int planetNo = 0;
};

void pass(std::unique_ptr < embeddedWorld > & ew) {

  cout << "inside Pass Function before the reset: planet no" << ew -> getPlanetNo() << endl;

  ew.reset(new embeddedWorld(15));

}

void passSourcePointer(const embeddedWorld * e) {

  cout << "inside Pass Pointer Function: planet no:" << e -> getPlanetNo() << endl;

}
void passSourceReference(const embeddedWorld & e) {

  cout << "inside Pass Reference Function: planet no:" << e.getPlanetNo() << endl;

}
int main() {

  { //passing unique_ptr's source as a pointer and reference
    std::unique_ptr < embeddedWorld > e1 = make_unique < embeddedWorld > (10);
    passSourcePointer(e1.get());
    passSourceReference( * e1);
  }

  cout << "*************************" << endl;

  { //passing unique_ptr as a reference - may reset
    std::unique_ptr < embeddedWorld > e2 = make_unique < embeddedWorld > (20);
    pass(e2);
    cout << "after the -pass- function plane no:" << e2 -> getPlanetNo() << endl;
  }

}

The output:

inside Pass Pointer Function: planet no:10
inside Pass Reference Function: planet no:10
inside embeddedWorld Destructor: planet No: 10
*************************
inside Pass Function before the reset: planet no20
inside embeddedWorld Destructor: planet No: 20
after the -pass- function plane no:15
inside embeddedWorld Destructor: planet No: 15

Any function which is taking a parameter as a unique_ptr reference may call the unique_ptr reset function like the pass function at the code above. If functions only use unique_ptr’s pointer It can take a reference and pointer, like the passSourcePointer and passReferencePointer functions.

 

Passing std::shared_ptr

When we pass the shared_ptr to functions we have to make sure its lifetime, whether it’s shared or not, whether reference count is increased or retained, etc. There is no best way to pass shared_ptr. The way depends on our purpose. Let’s check the ways on examples.

 

#include <iostream>

#include "memory"

using namespace std;

void share(std::shared_ptr < int > i) {

  cout << "inside the share function: use_count:" << i.use_count() << endl;

}
void reseat(std::shared_ptr < int > & i) {

  i.reset(new int(20));
  cout << "inside the reseat function: use_count:" << i.use_count() << endl;

}
void mayShare(const std::shared_ptr < int > & i) {

  cout << "inside the mayShare function: use_count:" << i.use_count() << endl;

}

int main() {

  {
    shared_ptr < int > i = make_shared < int > (10);
    share(i);
    cout << "after the share function, use_count: " << i.use_count() << endl;
  }

  cout << "*************************" << endl;

  {
    shared_ptr < int > i = make_shared < int > (10);
    reseat(i);
    cout << "after the reseat function, value: " << * i << endl;
  }

  cout << "*************************" << endl;

  {
    shared_ptr < int > i = make_shared < int > (10);
    mayShare(i);
  }

  return 0;
}

The output:

inside the share function: use_count:2
after the share function, use_count: 1
*************************
inside the reseat function: use_count:1
after the reseat function, value: 20
*************************
inside the mayShare function: use_count:1

 

According to the code above:

The function ‘void share(std::shared_ptr<int> i)’ takes a shared_ptr as a value. Inside the function, the copy constructor is invoked, then the reference count is increased. The overhead is sharing ownership with copy instructions. It extends the lifetime.

The function ‘void reseat(std::shared_ptr<int> & i)’ takes a shared_ptr as a non-const reference. The reference count is not increased. The function can reset the shared pointer.

The functions ‘void mayShare(const std::shared_ptr<int>& i)’ takes a const shared ptr as a reference. It’s the same as taking the object’s pointer or reference.

 

References:
C++ Core Guidelines
Microsoft: Example 5