License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright : Hervé Frezza-Buet, CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : smart.md

Table of contents

Smart pointers

Smart pointers are lighweight classes that provide instances behaving as pointers, while offering a lot of safety.

Null pointers

Do not use the former NULL macro (#define NULL 0). In C++11, the nullptr litteral is properly typed.

#include <iostream>

void f(int            i) {std::cout << "f(int)"            << std::endl;}
void f(int*           i) {std::cout << "f(int*)"           << std::endl;}
void f(double*        i) {std::cout << "f(double*)"        << std::endl;}
void f(std::nullptr_t i) {std::cout << "f(std::nullptr_t)" << std::endl;}
void g(int*           i) {std::cout << "g(int*)"           << std::endl;}

int main(int argc, char* argv[]) {
  int i;
  double j;
  void*  k;
  
  f(i);
  f(&i);
  f(&j);
  f(nullptr);
  // f(NULL);  <- ambiguity warning.
  // f(k);     <- ambiguity warning.
  
  int* p = nullptr;
  k      = nullptr;
  g(nullptr); // ok, g is not ambiguous.

  return 0;
}
/* Output :
   ------

f(int)
f(int*)
f(double*)
f(std::nullptr_t)
g(int*)

*/

Unique Pointers

Smart pointers offer smart management of the pointed memory (in the heap): No more delete, this is handled by the class so that you will not forget or misuse it.

This may cause some issues if more than one pointer refers the object. Such issues are solved by shared and weak pointers (see next), adding extra stuff. So if you do not need several pointers on the same objects, unique pointers to the job with no overhead.

Typing mechanism prevent from having too pointers on the same object, since this is not handle at the level of basic shared pointers.

#include <memory>
#include <num.hpp>

int main(int argc, char* argv[]) {
  {
    scope("Good usage");
    std::unique_ptr<num> p1;
    std::cout << std::boolalpha << "p1 is null? " << (p1 == nullptr) << std::endl; // Displays true.
    // p1 = new num("a", 10);                       <-- Compiling error !
    // std::unique_ptr<num> p2 = new num("a", 10);  <-- Compiling error !
    std::unique_ptr<num> p2 {new num("a", 10)};  // ok ?!?! Indeed, the constructor from num* is "explicit".
    // p1 = p2;                                     <-- Compiling error !
    p1 = std::move(p2);
    std::cout << std::boolalpha
	      << "p1 is null? "   << (p1 == nullptr)               // Displays false.
	      << ", p2 is null? " << (p2 == nullptr) << std::endl; // Displays true.
    auto p3 = std::make_unique<num>("b", 20);
    rem("p3 = std::move(p1);");
    p3 = std::move(p1);                                            // ("b", 20) is deleted here !
    ___;
    num n = *p3;  // p3 behaves as a pointer;
    //                                                                ("a", 10) is deleted here !
  }
  {
    scope("Never do this...");
    auto p1 = std::make_unique<num>("a", 10);
    std::unique_ptr<num> p2 {&(*p1)}; // We bypass the unique pointer protection.
    // p2 is poped out from the stack, it deletes the memory handled by p1.
    // p1 deletes the same memory again... double free corruption.
  }
  
  return 0;
}

Shared and weak pointers

Shares and weak pointers
Shares and weak pointers
#include <memory>
#include <num.hpp> // We use it for the variable name...
#include <list>

using student      = num;
using student_ref  = std::shared_ptr<student>;
using student_wref = std::weak_ptr<student>;
using population   = std::list<student_ref>;
using group        = std::list<student_wref>;

void kill(population& p, const std::string& target);
void print(group& g); // Not print(const group& g) ?!?!?

// In the comments, BAT(s, w) denotes the number s of shared pointer to
// batman and the number w of weak pointers to him.
// SUP(s, w) does the same for superman.

int main(int argc, char* argv[]) {
  population P;
  group      G1, G2, G3;
  {
    scope("Creating heroes...");
    student_ref batman {new student("Batman")}; // BAT(1, 0). Not the most efficient way... BAT(1, 0)
    auto superman       = std::make_shared<student>("Superman"); // SUP(1, 0)
    auto wonderwoman    = std::make_shared<student>("Wonder Woman");
    auto superdupont    = std::make_shared<student>("Super Dupont");
    auto casimir        = std::make_shared<student>("Casimir");
    auto electra        = std::make_shared<student>("Electra");
    rem("Building up population and groups");
    population heroes   = {batman, superman, wonderwoman, superdupont, electra}; // BAT(2, 0), SUP(2, 0). Yes, we have forgotten Casimir.
    group super_powered = {superman, wonderwoman, superdupont}; // SUP(2, 1)
    group female        = {wonderwoman, electra};
    group male          = {batman, superman, superdupont, casimir}; // BAT(2, 1), SUP(2, 2).
    rem("We affect local variables to main ones.");
    P  = heroes; // BAT(3, 1), SUP(3, 2). We could have moved (P = std::move(heroes)), but let us unefficiently copy.
    G1 = super_powered; // SUP(3, 3)
    G2 = female;
    G3 = male; // BAT(3, 2), SUP(3, 4)
    rem("Only Casimir is deleted here."); // as well as heroes, super_powered, female and male.
  }
  // BAT(1, 1), SUP(1, 2)
  {
    scope("Killing Wonder Woman"); // Yes, this can be done.
    kill(P, "Wonder Woman");
  }
  {
    scope("Group Status");
    print(G1); // "Superman" "Super Dupont"
    print(G2); // "Electra" 
    print(G3); // "Batman" "Superman" "Super Dupont"
  }
  return 0;
}

void kill(population& p, const std::string& target) {
  for(auto it = p.begin(); it != p.end(); ++it) {
    student_ref who = *it;
    if(std::string(*who) == target) {
      p.erase(it);
      return;
    }
  }
}

void print(group& g) {
  std::cout << scope_indent;
  // We clean up as we iterate.
  for(auto it = g.begin(); it != g.end(); /* nothing handeled here */)
    if(auto ref = it->lock(); ref) {/* thread safe, we get a shared from a weak... or nullptr */
      std::cout << '\"' << std::string(*ref) << "\" ";
      ++it;
    }
    else
      it = g.erase(it); // This is why g is not const...
  std::cout << std::endl;
}

Dereference and arithmetics

The smart pointers do not support addition/substraction with an integer, since they are not real addresses. You can use ptr->get() to get the actual address in the heap of the handled object.

Moreover, smart pointers support comparisions (<, <=, ==, …), as usual pointers do. It consists in comparing the address of the handled objects (the addresses are indeed integers).

Hervé Frezza-Buet,