Review of Parent – Child and Multi-Level Parent – Child Implementations

This is the sixth and final post in a series on implementing parent – child and multi-level parent – child relationships in C++. The previous posts are:

This post compares the various implementations for parent – child relationships: using raw pointers, using shared_ptrs, and using unique_ptrs. It also looks at the shared_ptr implementation for multi-level parent – child relationships. Finally, potential alternatives to these mechanisms are discussed.

Ownership

In the raw pointer implementation, the intent is that the Parent object owns its children (Child objects). However, this is not shown in any way except possibly in documentation. Looking at the code, a raw pointer is created when a Child object is created. Ownership of the Child object is transferred to the Parent object either in the Child constructor, or by calling Parent::addChild. But of course, the original pointer to the Child object still exists until that pointer goes out of scope. Both Parent::findChild and Parent::removeChild return a raw pointer to a Child object, but only Parent::removeChild transfers ownership back to the caller. As a result, there can be multiple pointers to the same Child object. It is left to the programmer to ensure that Child objects are destroyed appropriately, either automatically by the owning Parent object, or directly in her code for Child objects not owned by a Parent object. Any logic errors in the program can result in the code attempting to destroy a Child object multiple times (double deletion), or not at all (memory leak).

This situation is why smart pointers were invented. Let’s look first at the unique_ptr implementation. In this case, there is exactly one owner of a Child object and it is obvious who that owner is. The program must still ensure that the unique_ptr is valid before attempting to use it. The Child object is destroyed when the unique_ptr goes out of scope.

There is one problem area, however. Parent::findChild cannot return a unique_ptr to the Child object because the intent is for the Child object to still be owned by the Parent object; a raw pointer is thus returned from that method. The program must not delete the Child object pointed to by the raw pointer as that would leave a unique_ptr that points to a destroyed object.

Finally, in the shared_ptr implementation, Child objects have shared ownership. All references to a Child object are via shared_ptrs, even Parent::findChild. The underlying Child object is destroyed only when the last smart pointer to the object is destroyed.

Transfer of Ownership

In the raw pointer implementation, there are two ways of transferring ownership of the Child object to a Parent object: in the Child constructor, or via Parent::addChild.

In both the shared_ptr and unique_ptr implementations, ownership transfer to the parent is via the static method, Parent::addChild. A static method is used because smart pointers are required for both the Parent and Child objects; there is no way to get to the smart pointer to an object ( the Parent object) directly from that object. As I stated a couple of times in the implementation posts, I find static methods esthetically unsatisfying except possibly for builder and factory methods. That is a personal opinion; you, of course, may take the opposite view.

In the shared_ptr implementation, it may be possible to combine creation of the Child object with transfer of ownership to the Parent; a shared_ptr to the created Child object would be returned to the calling code. If this were tried in the unique_ptr implementation, a raw pointer to the Child object would have to be returned to the calling code because the Parent object must hold the unique_ptr to the Child object.

In all implementations, a Parent::addChild method (either static or not) is still required.

Separation of Responsibilities

The Child constructor in the raw pointer implementation performs two functions: construction of the Child object and possibly transfer of ownership of the object to a Parent object. Alternatively, a Child object can be constructed, and then via a call to Parent::addChild, ownership of the Child object can be transferred to the Parent object.

Performing both tasks in the constructor may be viewed as a violation of the separation of responsibilities, aka separation of concerns. In the smart pointer implementations, ownership transfer must be performed separately. Note however, that modified implementations of the smart pointer solutions could combine Child object construction and ownership transfer in a single method again violating separation of responsibilities.

Performing both tasks in a single method (constructor or builder method) hides some of the details at the wrong level of abstraction. Any programmer reading the code would need to know that the constructor (in the raw pointer implementation) does not just create a Child object but also transfers ownership of the object. This is not obvious because a constructor normally just creates an object and initializes its internal values.

Multi-level Parent – Child Relationships

The multi-level relationship is a generalization of the parent – child relationship. The only difference is that each Child object can in turn contain other Child objects, thereby functioning as both a Child object and as a Parent object.

The code provided in the implementation post assumes that all parent and child classes derive from Base; that is, every object created can have children. This normally does not occur. For instance, in a windowing system, there are container widgets (e.g. top-level windows, panels, group boxes) that can contain other widgets,  and controls (e.g. static text, buttons, edit controls, etc.) which cannot contain other widgets. In an n-ary tree implementation, there are normally two node types: leaf nodes and non-leaf nodes. Only the non-leaf nodes can have children (other nodes) attached to them.

Alternative Implementations of Parent – Child Relationships

This set of six posts used raw pointers, shared_ptrs and unique_ptrs to implement parent – child relationships. Only a shared_ptr implementation was provided for multi-level parent – child relationships. However, raw pointer and unique_ptr implementations of multi-level relationships are also possible. They, of course, suffer from the same problems that the raw pointer and unique_ptr implementations of parent – child relationships do, so see those posts for more information.

Alternative implementations are possible. We will discuss two: references to objects, and embedded reference counting.

References to Objects

In the raw pointer implementation of the parent – child relationship, each parent and child object was created on the heap. Alternatively, these objects could be created on the stack either within main, embedded in objects created within main, or as global objects. Instead of passing the objects via raw pointers, the objects would be passed by reference. Internally in the Parent and Child objects, the references would be stored as pointers. Returns from methods such as Parent::findChild and Parent::removeChild would be via raw pointers. Parent objects don’t own their children, simply have pointers to them, and the children are destroyed when they go out of scope.

The programmer must remember to never delete any Child object returned as a pointer.

Embedded Reference Counting

Before smart pointers were added to C++, Parent and Child classes often were implemented with internal reference counting. See for example, wxWidgets and Qt. The details are hidden from the programmer, but as long as the Parent and Child objects are created on the heap, the reference counting mechanism ensures that the objects are not deleted as long as they are being referenced.

Library designers today should consider implementing reference counting using smart pointers rather than rolling their own reference counting mechanisms.

 

Multi-Level Parent – Child Implementation: shared_ptr

The last few posts have discussed how to implement parent – child relationships using raw pointers, shared_ptrs, and unique_ptrs. In those relationships there are two object types: parent and child. This post will extend these relationships to allow any child to possibly be the parent of additional children. Such a relationship exists frequently with widgets in graphical user interface libraries. For example, a top-level window (main window or dialog) might contain a group box that in turn contains a number of checkboxes, buttons, and another group box that contains additional widgets. A drop-down menu contains menu items, some of which might be menus that contains other menu items, and so forth.

Here is a simple implementation, first the header file, and then the source file containing additional implementation code for the classes and a main function that exercises the classes. This code is available in the BaseDerived folder in my ParentChild GitHub repository.

// BaseDerived.h
#pragma once

#include <memory>
#include <vector>
#include <iostream>
#include <typeinfo>

enum ID {
    e1 = 1,
    e2, 
    e3,
    e4,
    e5
};

class Base {
public:
    explicit Base(ID id) : m_id(id) {}
    virtual ~Base() {}
    static void addChild(std::weak_ptr<Base> parent, std::shared_ptr<Base> child);
    std::shared_ptr<Base> Base::removeChild(ID id);
    std::shared_ptr<Base> removeChild(std::shared_ptr<Base>& pChild)
    {
        return removeChild(pChild->getID());
    }
    std::shared_ptr<Base> findChild(ID id);
    ID getID() { return m_id; }
    friend bool operator==(const Base& lhs, const Base& rhs);
    friend std::ostream& operator<<(std::ostream& os, const Base& base);
private:
    void setParent(std::weak_ptr<Base>& wParent) { m_pParent = wParent; }
    void clearParent();

    std::weak_ptr<Base> m_pParent;
    std::vector < std::shared_ptr<Base>> m_children;
    ID m_id;
};

bool operator==(const Base& lhs, const Base& rhs)
{
    return lhs.m_id == rhs.m_id;
}

std::ostream& operator<<(std::ostream& os, const Base& base)
{
    os << typeid(base).name() << ": id = " << base.m_id << "\n";
    for (auto& pChild : base.m_children) {
        auto wB = pChild->m_pParent;
        while (wB.lock()) {
            os << "\t";
            std::shared_ptr<Base> pB = wB.lock();
            wB = pB->m_pParent;
        }
        os << *pChild;
    }
    return os;
}

class A : public Base
{
public:
    explicit A(ID id) : Base(id) {}
};

class B : public Base 
{
public:
    explicit B(ID id) : Base(id) {}
};

class C : public Base
{
public:
    explicit C(ID id) : Base(id) {}
};

// BaseDerived.cpp

#include <algorithm>
#include "BaseDerived.h"

void Base::addChild(std::weak_ptr<Base> pParent, std::shared_ptr<Base> pChild)
{
    auto sParent = pParent.lock();
    if (sParent && sParent == pChild) {
        return;
    }
    if (auto pChildsParent = pChild->m_pParent.lock()) {
        pChildsParent->removeChild(pChild);
    }
    sParent->m_children.push_back(pChild);
    pChild->setParent(pParent);
}

std::shared_ptr<Base> Base::removeChild(ID id) {
    Base child(id);
    auto iter = std::find_if(m_children.begin(), m_children.end(),
        [&child](const auto& ch) ->bool { return child == *ch; });
    if (iter != m_children.end()) {
        auto rChild = *iter;
        m_children.erase(iter);
        rChild->clearParent();
        return rChild;
    }
    else {
        return std::shared_ptr<Base>();
    }
}

std::shared_ptr<Base> Base::findChild(ID id)
{
    Base child(id);
    auto iter = std::find_if(m_children.begin(), m_children.end(),
        [&child](const auto& ch) ->bool { return child == *ch; });
    if(iter != m_children.end()) {
        return *iter;
    }
    else {
        return std::shared_ptr<Base>();
 }
}

void Base::clearParent()
{
    if (auto pParent = m_pParent.lock()) {
        pParent->removeChild(m_id);
    }
    setParent(std::weak_ptr<Base>());
}

int main()
{
    std::shared_ptr<A> pA1 = std::make_shared<A>(e1);
    std::shared_ptr<B> pB1 = std::make_shared<B>(e2);
    std::shared_ptr<B> pB2 = std::make_shared<B>(e3);
    std::shared_ptr<C> pC1 = std::make_shared<C>(e4);
    std::shared_ptr<C> pC2 = std::make_shared<C>(e5);
    std::weak_ptr<A> wA1 = pA1;
    std::weak_ptr<B> wB1 = pB1;
    A::addChild( wA1, pB1);
    A::addChild(wA1, pB2);
    B::addChild(wB1, pC1);
    B::addChild(wB1, pC2);
    std::cout << *pA1;

    std::shared_ptr<Base> pB3 = pA1->removeChild(e3);
    std::weak_ptr<C> wC2 = pC2;
    C::addChild(wC2, pB3);
    std::cout << '\n' << *pA1;

    std::shared_ptr<Base> pB4 = pA1->findChild(e2);
    std::cout << "\npB4: " << *pB4;
    std::cout << "\npA1 after findChild: " << *pA1;

    std::cout << "\nPress any non-whitespace character key and then the Enter key to terminate:" << std::endl;
    char c;
    std::cin >> c;
}

Here is the output from the program:

class A: id = 1
    class B: id = 2
        class C: id = 4
        class C: id = 5
    class B: id = 3

class A: id = 1
    class B: id = 2
        class C: id = 4
        class C: id = 5
            class B: id = 3

pB4: class B: id = 2
        class C: id = 4
        class C: id = 5
            class B: id = 3

pA1 after findChild: class A: id = 1
    class B: id = 2
        class C: id = 4
        class C: id = 5
            class B: id = 3

Things to Note:

  1. The base class contains a vector of shared_ptrs to children objects.
  2. The base class contains a weak_ptr to a parent object.
  3. Classes A, B, and C are all derived from Base.
  4. Base::addChild is a static method that takes both a weak_ptr to a parent object and a shared_ptr to the child to add to the children held by the parent. After appropriate checks, the shared_ptr to the child is added to the vector of child objects in the parent pointed to by the weak_ptr, and then sets the added child’s parent to the value of the weak_ptr.
  5. There are two Base::removeChild methods, both of which return a shared_ptr<Base> to the child object being removed.
  6. Base::findChild also returns a shared_ptr<Base> to the child object being removed.

Analysis of This Solution

The previous posts looked at the pros and cons of each approach. Since I am providing only one approach here to coding multi-level parent-child relationships, I have decided to simply discuss the solution.

  1. This solution is similar to the parent – child shared_ptr solution.
  2. It is possible to implement this multi-level relationship using raw pointers and unique_ptrs as well, but those implementations suffer from the same potential problems that were listed in the parent – child posts.
  3. As I noted in the parent – child shared_ptr and unique_ptr implementations, a static method (or a function) is needed to add a child to a parent object because smart pointers to both the parent and child objects are required. I find this esthetically unsatisfying.

The final post in this series will review and compare the parent – child and multi-level parent – child relationships.

Parent – Child Implementation: unique_ptr

This is the fourth in a series of posts on parent – child relationships. See

for the first three posts.

In this post we will look at implementing a parent – child relationship using unique_ptr. unique_ptr is insufficient by itself to implement parent – child relationships; shared_ptrs and raw pointers are also used in the example below. The pros and cons of this approach are also discussed.

Here is some sample code. There are two files: ParentChild.h, which declares the Parent and Child classes as well as providing some of their implementation, and UniquePointer.cpp, which provides the rest of the Parent and Child class implementations, and a main function that exercises the classes. This code is provided, along with a Visual Studio project in the UniquePointer folder on GitHub.

// ParentChild.h
#pragma once

#include <vector>
#include <algorithm>
#include <iostream>
#include <memory>
#include <assert.h>

enum ID {
    e1 = 1,
    e2,
    e3
};

class Parent;

class Child {
public:
    explicit Child(ID id) : m_id(id) {}
    ~Child() noexcept {}
    const ID getID() const noexcept { return m_id; }
    friend std::ostream& operator<<(std::ostream& os, const Child* pChild);
    friend Parent;
private:
    void setParent(std::shared_ptr<Parent>& pParent)
    {
        m_pParent = pParent;
    }

    std::shared_ptr<Parent> m_pParent;
    ID m_id;
};

bool operator==(const Child& lhs, const Child& rhs) noexcept {
    return lhs.getID() == rhs.getID();
}

std::ostream& operator<<(std::ostream& os, const Child* pChild) {
    if (pChild) {
        os << "parent = ";
        if (pChild->m_pParent) {
            os << pChild->m_pParent << ", id = " << pChild->m_id << '\n';
        }
        else {
            os << "nullptr, id = " << pChild->m_id << '\n';
        }
    }
    else {
        os << "nullptr\n";
    }
    return os;
}


class Parent {
public:
    Parent() {}
    ~Parent() noexcept {}
    static void addChild(std::shared_ptr<Parent>& pParent, std::unique_ptr<Child>& pChild) {
        pChild->setParent(pParent);
        pParent->m_children.push_back(std::forward<std::unique_ptr<Child>>(pChild));
    }
    std::unique_ptr<Child> removeChild(ID id);
    std::unique_ptr<Child> removeChild(Child* pChild) {
        return removeChild(pChild->getID());
    }
    Child* findChild(ID id);
    friend std::ostream& operator<<(std::ostream& os, const std::shared_ptr<Parent>& pParent);
private:
    std::vector<std::unique_ptr<Child>> m_children;
};

std::ostream& operator<<(std::ostream& os, const std::shared_ptr<Parent>& pParent) {
    if (pParent) {
        os << "Parent =" << pParent.get() << ":\n";
        for (const std::unique_ptr<Child>& pChild : pParent->m_children) {
            os << "Child: " << pChild.get();
        }
    }
    else {
        os << "Parent = nullptr\n";
    }
    return os;
}
// UniquePointer.cpp
#include "ParentChild.h"

std::unique_ptr Parent::removeChild(ID id) {
    std::unique_ptr pChild = std::make_unique(id);
    auto iter = std::find_if(m_children.begin(), m_children.end(),
        [&pChild](const auto& ch) ->bool { return *pChild == *ch; });
    if (iter != m_children.end()) {
        m_children.erase(iter);
        return pChild;
    }
    else {
        return std::unique_ptr(nullptr);
    }
}

Child* Parent::findChild(ID id) {
    std::unique_ptr pChild = std::make_unique(id);
    auto iter = std::find_if(m_children.begin(), m_children.end(),
        [&pChild](const auto& ch) ->bool { return *pChild == *ch; });
    if (iter != m_children.end()) {
        return iter->get();
    }
    else {
        return nullptr;
    }
}

int main()
{
    std::shared_ptr pParent = std::make_shared();
    std::unique_ptr pChild1 = std::make_unique(e1);
    std::cout << "pChild1 after create: " << pChild1.get();
    std::cout << "pParent after pChild1 created: " << pParent;
    Parent::addChild(pParent, pChild1);
    std::cout << "pParent after pChild1 added: " << pParent;
    std::cout << "pChild1 after addChild: " << pChild1.get();
    std::unique_ptr pChild2 = std::make_unique(e2);
    std::unique_ptr pChild3 = std::make_unique(e3);
    Parent::addChild(pParent, pChild3);
    std::cout << "pParent after pChild3 added: " << pParent;  Child* pChild4 = pParent->findChild(e1);
    std::cout << "pChild4: " << pChild4;
    std::cout << "pParent after findChild call: " << pParent;  auto pChild5 = pParent->removeChild(e3);
    std::cout << "pChild5: " << pChild5.get();
    std::cout << "pParent after removeChild(e3): " << pParent;  std::unique_ptr pChild6 = pParent->removeChild(pChild4);
    std::cout << "pChild6: " << pChild6.get();
    std::cout << "pParent after removeChild(pChild6): " << pParent;  auto pChild7 = pParent->removeChild(e1);
    std::cout << "pChild7: " << pChild7.get();  auto pChild8 = pParent->removeChild(pChild4);
    std::cout << "pChild8: " << pChild8.get();

    std::cout << "Press any non-whitespace character key and then the Enter key to terminate:" << std::endl;
    char c;
    std::cin >> c;
}

A number of things should be noted:

  1. The Child class contains an ID property which is used to distinguish between the various Child objects.
  2. The Parent class provides two methods for removing children: the first method uses the ID parameter to locate the Child object to remove, and the second method uses a shared_ptr to the Child object to remove.
  3. Parent::findChild finds the Child object with the specified ID. It does not remove the Child object from the list of Child objects in the Parent object.
  4. The Parent and Child classes form a circular reference to each other, with each Parent object holding a unique_ptr to each of the children that it owns, and each Child object holding a shared_ptr to its Parent object. If the Parent objects held a shared_ptr to their Child object, then the Parent and Child objects could never be destroyed; the reference counts would never reach 0.
  5. Parent::addChild is a static method that takes a shared_ptr to the Parent object, and a unique_ptr to the Child object to be added. The method must be static because a shared_ptr is required to the Parent object, and the program cannot generate one directly from the Parent object itself.
  6. Because Parent::addChild is a static method, it could alternatively be Child::addToParent with the same arguments. Lastly, it could be a function, but since it is related to Parent and Child, either of the static methods would normally be used.
  7. Parent::findChild returns a raw pointer to the Child object. It cannot return a shared_ptr, weak_ptr, or unique_ptr because the Parent object uses a unique_ptr to reference the Child object. A unique_ptr cannot be converted to a shared_ptr. If a unique_ptr to the Child object is returned, then the unique_ptr stored by the Parent object would be invalidated and ownership would be transferred to the calling object.
  8. Parent::removeChild returns a unique_ptr to the Child object being removed because ownership must be transferred from the Parent object to the object that called Parent::removeChild.

Pros

  1. Ownership of the Child objects is via a unique_ptr, and ownership of Parent objects is via a shared_ptr, so there is no need for the coder to keep track of when the objects must be deleted.
  2. No code is needed in the Parent destructor to destroy the Child objects because use of unique_ptr ensures that each Child object is automatically deleted when the vector holding the Child objects is destroyed.

Cons

  1. unique_ptrs must be moved rather than copied, so both a move constructor and a move operator= method must be implemented for the Child class. This is extra code to maintain which might not otherwise be needed.
  2. A function or static method is required for adding a Child object to a Parent object. Beyond object initialization using builder or factory methods, I find static methods to be esthetically unsatisfying.
  3. This implementation uses a combination of raw pointers, unique_ptrs, and shared_ptrs. Therefore, it suffers from most or all of the problems that the implementation for each pointer type suffers from. See the posts for the other implementations to view those problems.

Parent – Child Implementation: shared_ptr

This is the third in a series of posts on parent – child relationships. See

for the first two posts.

In this post we will look at implementing a parent – child relationship using shared_ptr and weak_ptr, and discuss the pros and cons of this approach.

Here is some sample code. There are two files: ParentChild.h, which declares the Parent and Child classes as well as providing some of their implementation, and SharedPointer.cpp, which provides the rest of the Parent and Child class implementations, and a main function that exercises the classes. This code is provided, along with a Visual Studio project in the SharedPointer folder on GitHub.

// ParentChild.h
#pragma once

#include <vector>
#include <algorithm>
#include <iostream>
#include <memory>

enum ID {
    eNoID = 0,
    e1,
    e2,
    e3
};

class Parent;

class Child {
public:
    explicit Child(ID id) : m_pParent(std::weak_ptr<Parent>()), m_id(id) {}
    ~Child() noexcept {}
    const ID getID() const noexcept { return m_id; }
    void clearParent();
    friend std::ostream& operator<<(std::ostream& os, const std::shared_ptr<Child>& pChild);
    friend Parent;
private:
    void setParent(std::weak_ptr<Parent>& pParent) noexcept { m_pParent = pParent; }
    std::weak_ptr<Parent> m_pParent;
    ID m_id;
};

bool operator==(const Child& lhs, const Child& rhs) noexcept {
    return lhs.getID() == rhs.getID();
}

std::ostream& operator<<(std::ostream& os, const std::shared_ptr<Child>& pChild) {
    if (pChild) {
        os << "parent = ";
        if (auto pParent = pChild->m_pParent.lock()) {
            os << pParent << ", id = " << pChild->m_id;
            os << ", use_count = " << pChild.use_count() << '\n';
        }
        else {
            os << "nullptr, id = " << pChild->m_id;
            os << ", use_count = " << pChild.use_count() << '\n';
        }
    }
    else {
        os << "nullptr\n";
    }
    return os;
}

class Parent {
public:
    Parent() {}
    ~Parent() noexcept {}
    static void addChild(std::weak_ptr<Parent>& wParent, std::shared_ptr<Child>& pChild) {
        if (auto pParent = wParent.lock()) {
            pChild->setParent(wParent);
            pParent->m_children.push_back(pChild);
        }
    }
    std::shared_ptr<Child> removeChild(ID id);
    std::shared_ptr<Child> removeChild(std::shared_ptr<Child>& pChild) {
        return removeChild(pChild->getID());
    }
    auto findChild(ID id);

    friend std::ostream& operator<<(std::ostream& os, const std::shared_ptr<Parent>& pParent);
private:
    std::vector<std::shared_ptr<Child>> m_children;
};

std::ostream& operator<<(std::ostream& os, const std::shared_ptr<Parent>& pParent) {
    if (pParent) {
        os << "Parent =" << pParent.get() << ":\n";
        for (const std::shared_ptr<Child>& pChild : pParent->m_children) {
            os << "Child: " << pChild;
        }
    }
    else {
        os << "Parent = nullptr\n";
    }
    return os;
}
// SharedPointer.cpp

#include "ParentChild.h"

void Child::clearParent() 
{
    if (auto pParent = m_pParent.lock()) {
        pParent->removeChild(m_id);
    }
    setParent(std::weak_ptr<Parent>());
}

std::shared_ptr<Child> Parent::removeChild(ID id) {
    Child child(id);
    auto iter = std::find_if(m_children.begin(), m_children.end(),
       [&child](const auto& ch) ->bool { return child == *ch; });
    if (iter != m_children.end()) {
        auto rChild = *iter;
        m_children.erase(iter);
        rChild->clearParent();
        return rChild;
    }
    else {
        return std::shared_ptr<Child>(nullptr);
    }
}

auto Parent::findChild(ID id) {
    Child child(id);
    auto iter = std::find_if(m_children.begin(), m_children.end(),
        [&child](const auto& ch) ->bool { return child == *ch; });
    if (iter != m_children.end()) {
        return *iter;
    }
    else {
        return std::shared_ptr<Child>(nullptr);
    }
}

int main()
{
    std::shared_ptr<Parent> pParent = std::make_shared<Parent>();
    std::weak_ptr<Parent> wParent = pParent;
    std::shared_ptr<Child> pChild1 = std::make_shared<Child>(e1);
    Parent::addChild(wParent, pChild1);
    std::cout << "pChild1: " << pChild1;
    std::cout << "pParent after creating pChild1: " << pParent;
    std::shared_ptr<Child> pChild2 = std::make_shared<Child>(e2);
    std::cout << "pChild2 before addChild call: " << pChild2;
    Parent::addChild(wParent, pChild2);
    std::cout << "pParent after addChild call: " << pParent;
    std::shared_ptr<Child> pChild3 = std::make_shared<Child>(e3);
    Parent::addChild(wParent, pChild3);
    std::cout << "pParent after three children added:\n" << pParent;
    std::shared_ptr<Child> pChild4 = pParent->findChild(e2);
    std::cout << "pChild4: " << pChild4;
    std::cout << "pParent after findChild call: " << pParent;
    std::shared_ptr<Child> pChild5 = pParent->removeChild(e1);
    std::cout << "pChild5: " << pChild5;
    std::cout << "pParent after removeChild(e1): " << pParent;
    std::shared_ptr<Child> pChild6 = pParent->removeChild(pChild4);
    std::cout << "pChild6: " << pChild6;
    std::cout << "pParent after removeChild(pChild4): " << pParent;
    std::shared_ptr<Child> pChild7 = pParent->removeChild(e1);
    std::cout << "pChild7: " << pChild7;
    std::shared_ptr<Child> pChild8 = pParent->removeChild(pChild4);
    std::cout << "pChild8: " << pChild8;

    pParent.reset();
    std::cout << "pParent after reset: " << pParent;

    std::cout << "\nPress any non-whitespace character key and then the Enter key to terminate:" << std::endl;
    char c;
    std::cin >> c;
}

Here is the output from running the program:

pChild1: parent = 000001C462155650, id = 1, use_count = 2
pParent after creating pChild1: Parent =000001C462155650:
Child: parent = 000001C462155650, id = 1, use_count = 2
pChild2 before addChild call: parent = nullptr, id = 2, use_count = 1
pParent after addChild call: Parent =000001C462155650:
Child: parent = 000001C462155650, id = 1, use_count = 2
Child: parent = 000001C462155650, id = 2, use_count = 2
pParent after three children added:
Parent =000001C462155650:
Child: parent = 000001C462155650, id = 1, use_count = 2
Child: parent = 000001C462155650, id = 2, use_count = 2
Child: parent = 000001C462155650, id = 3, use_count = 2
pChild4: parent = 000001C462155650, id = 2, use_count = 3
pParent after findChild call: Parent =000001C462155650:
Child: parent = 000001C462155650, id = 1, use_count = 2
Child: parent = 000001C462155650, id = 2, use_count = 3
Child: parent = 000001C462155650, id = 3, use_count = 2
pChild5: parent = nullptr, id = 1, use_count = 2
pParent after removeChild(e1): Parent =000001C462155650:
Child: parent = 000001C462155650, id = 2, use_count = 3
Child: parent = 000001C462155650, id = 3, use_count = 2
pChild6: parent = nullptr, id = 2, use_count = 3
pParent after removeChild(pChild4): Parent =000001C462155650:
Child: parent = 000001C462155650, id = 3, use_count = 2
pChild7: nullptr
pChild8: nullptr
pParent after reset: Parent = nullptr

A number of things should be noted:

  1. The Child class contains an ID property which is used to distinguish between the various Child objects.
  2. The Parent class provides two methods for removing children: the first method uses the ID parameter to locate the Child object to remove, and the second method uses a shared_ptr to the Child object to remove.
  3. Parent::findChild finds the Child object with the specified ID. It does not remove the Child object from the list of Child objects in the Parent object.
  4. The Parent and Child classes form a circular reference to each other, with each Parent object holding a shared_ptr to each of the children that it owns, and each Child object holding a weak_ptr to its Parent object. If the Child objects held a shared_ptr to their Parent object, then the Parent and Child objects could never be destroyed; the reference counts would never reach 0.
  5. Parent::addChild is a static method that takes a weak_ptr to the Parent object, and a shared_ptr to the Child object to be added. The method must be static because a weak_ptr is required to the Parent object, and the program cannot generate one directly from the Parent object itself.
  6. Because Parent::addChild is a static method, it could alternatively be Child::addToParent with the same arguments. Lastly, it could be a function, but since it is related to Parent and Child, either of the static methods would typically be used.
  7. Unlike the raw pointer implementation, there is no need to keep track of who owns the Child objects because they are created as the targets of shared_ptrs and are therefore deleted when the reference count of each shared_ptr goes to 0.

Pros

  1. Ownership of the Child objects is shared, so there is no need for the coder to keep track of when the objects must be deleted.
  2. No code is needed in the Parent destructor to destroy the Child objects because the reference counts for the Child objects are automatically decremented when the vector holding the Child objects is destroyed.

Cons

  1. There is greater overhead copying shared_ptrs than copying raw pointers. In most programs, this overhead would be a small very portion of the total program execution time.
  2. A function or static method is required for adding a Child object to a Parent object. Beyond object initialization using builder or factory methods, I find static methods to be esthetically unsatisfying.
  3. There are a number of subtle potential bugs when using shared_ptr. See Dangers of std::shared_ptr for details.