Solve Smart Pointer Cycle Reference

What is the Cycle Reference?

  • When two objects rely on each other, and thus there is one shared_ptr in each of the two objects, and each shared_ptr points at another object.
  • For example, one computer should have a socket to a screen, and the screen is the opposite but similar.
class Computer
{
private:
    std::shared_ptr<Screen> pScreen;
};
class Screen
{
private:
    std::shared_ptr<Computer> pComputer;
};
  • Note that When we utilize make_shared to allocate dynamic memory and connect one Computer to a Screen, the reference count of any shared_ptr is TWO. Therefore, when one of two objects is released, the reference count of another object’s shared_ptr will NOT reduce to 0 – Memory Leak!

Cycle Reference Demo Code

  • The code below simulates a scenario where Cycle Reference causes Memory Leak.
C++
class OS;
class Screen;
class Computer
{
public:
    friend OS;
    Computer() { std::cout << "Create Computer\n"; }
    ~Computer() { std::cout << "GC>> computer\n"; }

private:
    std::shared_ptr<Screen> pScreen;
};

class Screen
{
public:
    friend OS;
    Screen() { std::cout << "Create Screen\n"; }
    ~Screen() { std::cout << "GC>> screen\n"; }

private:
    std::shared_ptr<Computer> pComputer;
};

class OS
{
public:
    OS() = default;
    bool static connect(std::shared_ptr<Computer> computer, std::shared_ptr<Screen> screen)
    {
        computer->pScreen = screen;
        screen->pComputer = computer;
        return true;
    }
};

int main(void)
{
    std::weak_ptr<Computer> probeComputer;
    std::weak_ptr<Screen> probeScreen;
    {
        std::shared_ptr<Computer> computer = std::make_shared<Computer>();
        std::shared_ptr<Screen> screen = std::make_shared<Screen>();
        probeComputer = computer;
        probeScreen = screen;
        OS::connect(computer, screen);

        std::cout << std::format("Existing Computer>>\t{}\n", probeComputer.use_count());
        std::cout << std::format("Existing Screen>>\t{}\n", probeComputer.use_count());
    }
    std::cout << std::format("Existing Computer>>\t{}\n", probeComputer.use_count());
    std::cout << std::format("Existing Screen>>\t{}\n", probeComputer.use_count());
    return EXIT_SUCCESS;
}

/*[SYSTEM OUTPUT]----------------------------------------------------------------------
  Create Computer
  Create Screen
  Existing Computer>>     2
  Existing Screen>>       2
  Existing Computer>>     1
  Existing Screen>>       1
-------------------------------------------------------------------------------------*/

How to Avoid Cycle Reference?

Method A: weak_ptr

  • The reference count will not increase when one weak_ptr points at shared_ptr, so we can replace one shared_ptr with weak_ptr.
  • Conventionally, the subordinary class should use weak_ptr, in other words, there is a Has-A relationship between classes. In the situation above, the screen should be a subordinate of the computer.
C++
class Screen
{
public:
    friend OS;
    Screen() { std::cout << "Create Screen\n"; }
    ~Screen() { std::cout << "GC>> screen\n"; }

private:
    std::weak_ptr<Computer> pComputer; // [MODIFIED]
};

Method B: Reachability Analysis

  • Here, I use one Garbage Collection strategy in JVM(Java Virtual Machine) as an example, but I will not introduce much about this method because I have no experience with JVM!
  • In short, GC Roots are some important objects in JVM system, which should exist during a whole program life span and have no mutual reference with other objects like Hardware, OS, etc. in our computer (They are cascading)
  • When GC happens, one probe will traverse the Object Tree from each GC Root. The objects did not be visited will be released after traversing, like Object4,5,6 in the figure above even though there are references.
  • More specifically, in our example, when OS is freed, the reference between OS and Hardware also be cut, and thus the GC can not reach OS from Hardware(GC Root).

Leave a Reply

Your email address will not be published. Required fields are marked *