// This is a small example (or introduction) on Dynamic Memory and Smart Pointers in C++
// Execute it in your IDE or (more simple) use an online compiler like https://www.programiz.com/cpp-programming/online-compiler/
// You find more details and good examples on https://www.learncpp.com (Chapter M) which was my inspiration
// Timon Erhart (HSR/OST) / 04.01.2021

#include <iostream>
#include <memory> // for the Smart Pointers

using namespace std;

struct Object{
    Object(){
        cout << "Object created\n";
    }
    void speak(){
        cout << "Hello\n";
    }
    ~Object(){
        cout << "Object deleted\n";
    }
};

int main() {
    
    // Stack example
    cout << "\n** Stack example ***\n";
    {
        Object o{}; // Object lives on the stack. Automatically created and destroyed when out of scope
        o.speak();
    } // (end of scope)
    
    // Heap example
    cout << "\n*** Heap example ***\n";
    {
        Object * op = new Object{}; // Create object on heap an return a pointer
		op->speak();
        delete op; // Calls destructor and deletes the object
        //WARNING: if you forget the delete -> Memory leak
        //WARNING: if you call delete twice -> Undefined behavior (most certainly destroying the whole universe)
    }
    
    // ### Therefore be smart and use Smart Pointers ###
 
    // Unique pointer: They calls new in its constructor and delete in its destructor. As the unique_ptr lives on the stack, it is quaranteed to call its	destructor (likewhise the first example)
     cout << "\n*** Unique pointer example ***\n";
    {
        unique_ptr<Object> up{ new Object() }; // Creates the object
        up->speak(); // Use like a normal pointer with ->
        (*up).speak(); // or dereferencing with *
        // (No explicit delete necessary, gets deleted automatically when 'up' is out of scope). Manual release with .reset()
        
        //auto up2 = up; // IMPORTANT: You can NOT make a copy of a unique pointer or passing it to a function (Compiler error)
        
        // DONT: But you can still trick yourself and shoot your foot (we are still in C++)
        //unique_ptr<Object> up2{ up }; // delete will be called twice when up and up2 gets out of scope -> Undefined behavior

    }   
    
      // Shared pointer: Works similar to unique pointer, but can be copied. The shared_ptr holds a counter and with every copy it makes counter++. Likewise when a copy is destroyed it makes counter-- and calling delete only if the counter is 0.
      cout << "\n*** Shared pointer example ***\n";
    {
        shared_ptr<Object> sp{ new Object() }; // Creates the object
        auto sp2 = make_shared<Object>(); // BETTER use the provided factory-function which returns the shared_pointer (more efficient)
		
        auto sp3 = sp; // Copying the shared_pointer is allowed (still same Object on heap). Can also be passed to a function as parameter
        shared_ptr<Object> sp4{ sp }; // Another way to make a copy of the pointer
		Object o2{*sp}; // REAL COPY: Copies 'sp' from the heap to the stack into 'o2'
		sp3.reset(); // Releases the pointer (and counter--). If counter==0, the object is deleted.
		
    }    
    
    // Weak pointer: Shared pointer can be a problem if two objects hold pointers to each other inside it. This is called a Circular References and end up in a deadlock.  Neither of them gets deleted -> memory leak (...and they will live happily ever after).
          
    // Example how to create a circular reference:
          // - class Person holds a variable shared_ptr<Person> friend;
          // - Person lucky is a friend of mike
          // - Person mike is a friend of lucky
          // HINT: It does not necessary need two instances: This can also happen with just one person, who he is friend with himself (maybe because he is schizophrenic).
          
    // To solve this problem, weak_ptr's where invented. A weak_ptr does not really store the objects reference. To access the Object, you have to 'convert' the weak_ptr first to a shared_ptr by calling lock(), which returns a shared_ptr. So it does only increment the shared_pointer counter during the time when its used and decrement it when its finish.
          
    cout << "\n*** Weak pointer example ***\n";
    // This is just a small example do shows the mechanics
    // You can find a good and detailed example here: https://www.learncpp.com/cpp-tutorial/circular-dependency-issues-with-stdshared_ptr-and-stdweak_ptr/
    weak_ptr<Object> wp; // (Note, the weak pointer is outside scope of shared_ptr sp)
    {
        shared_ptr<Object> sp{ new Object }; // Create the object
        wp = sp; // Assign the shared_ptr to the weak_ptr (this does not increase the share pointer's counter yet)
        shared_ptr<Object> sp2 = wp.lock(); // Get a shared pointer out of the weak_ptr (this will increase the counter, but only as long a sp2 lives)
        sp2->speak(); // Use the weak pointer
    } // sp and sp2 get deleted, so the Object get deleted
    
    shared_ptr<Object> sp3 = wp.lock(); // If you call lock() on a already deleted object, it will return NULL;
    cout << "is sp3 NULL? " << std::boolalpha << (sp3 == NULL) << '\n'; // True
    cout << "is wp  expired? " << wp.expired() << '\n'; // A better way is to check with expired()
    // BEST PRACTICE: So you most likely want to do something like this:
    if (!wp.expired()){
        (wp.lock())->speak();
    } else {
        cout << "sorry, but the object has already died :(\n";
    }

}