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.
Advertisements

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.

Parent – Child Implementation: Raw Pointers

This is a follow-on post from Implementing Parent – Child and Similar Relationships. In this post we will look at implementing a parent – child relationship using raw pointers 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 RawPointer.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 RawPointer folder on GitHub.

// ParentChild.h
#pragma once

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

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

class Parent;

class Child {
public:
    explicit Child(ID id) : Child(nullptr, id) {}
    Child(Parent* parent, 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(Parent* pParent) noexcept { m_parent = pParent; }
    Parent* m_parent;
    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 = " << pChild->m_parent << ", id = " << pChild->m_id << '\n';
    }
    else {
        os << "nullptr\n";
    }
    return os;
}

class Parent {
public:
    Parent() {}
    ~Parent() noexcept {
        while (m_children.size() > 0) {
            auto pChild = removeChild(m_children[0]->getID());
            delete pChild;
        }
    }
    void addChild(Child* child) {
        child->setParent(this);
        m_children.push_back(child);
    }
    Child* removeChild(ID id) {
        Child child(nullptr, 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()) {
            Child* rChild = *iter;
            m_children.erase(iter);
            rChild->setParent(nullptr);
            return rChild;
        }
        else {
            return nullptr;
        }
    }
    Child* removeChild(Child* pChild) {
        return removeChild(pChild->getID());
    }
    Child* findChild(ID id) {
        Child child(nullptr, 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 nullptr;
        }
    }
    friend std::ostream& operator<<(std::ostream& os, const Parent& parent);
private:
    std::vector<Child*> m_children;
};

std::ostream& operator<<(std::ostream& os, const Parent& parent) {
    os << "Parent =" << &parent << ":\n";
    for (const auto& pChild : parent.m_children) {
       os << "Child: " << pChild;
    }
    return os;
}
// RawPointer.cpp
#include "ParentChild.h"

Child::Child(Parent* parent, ID id) : m_parent(parent), m_id(id) {
    if (parent != nullptr) {
        parent->addChild(this);
    }
}

Child::~Child() noexcept {
    if (m_parent != nullptr) {
        m_parent->removeChild(this);
    }
}

Child* addChild3(Parent* parent)
{
    Child* pChild = new Child(parent, e3);
    return pChild;
}

int main()
{
    Parent* pParent = new Parent();
    Child* pChild1 = new Child(pParent, e1);
    std::cout << "child1: " << pChild1;
    Child* pChild2 = new Child(e2);
    std::cout << "child2 before addChild call: " << pChild2;
    pParent->addChild(pChild2);
    std::cout << "child2 after addChild call: " << pChild2;
    Child* pChild3 = addChild3(pParent);
    std::cout << "child3: " << pChild3;
    std::cout << "Parent after three children added:\n" << *pParent << '\n';
    Child* pChild4 = pParent->findChild(e2); // return pointer to child2
    std::cout << "Parent after findChild call:\n" << *pParent << '\n';
    std::cout << "child4: " << pChild4;
    Child* pChild5 = pParent->removeChild(e1);
    std::cout << "child5: " << pChild5;
    std::cout << "Parent after removeChild(e1) call:\n" << *pParent << '\n';
    Child* pChild6 = pParent->removeChild(pChild4);
    std::cout << "child6: " << pChild6;
    std::cout << "Parent after removeChild(pChild4) call:\n" << *pParent << '\n';
    Child* pChild7 = pParent->removeChild(e1);
    std::cout << "pChild7: " << pChild7;
    Child* pChild8 = pParent->removeChild(pChild4);
    std::cout << "pChild8: " << pChild8;

    delete pParent;
    delete pChild5;
    delete pChild6;

   std::cout << "Press 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:

child1: parent = 0000010DA7B008B0, id = 1
child2 before addChild call: parent = 0000000000000000, id = 2
child2 after addChild call: parent = 0000010DA7B008B0, id = 2
child3: parent = 0000010DA7B008B0, id = 3
Parent after three children added:
Parent =0000010DA7B008B0:
Child: parent = 0000010DA7B008B0, id = 1
Child: parent = 0000010DA7B008B0, id = 2
Child: parent = 0000010DA7B008B0, id = 3

Parent after findChild call:
Parent =0000010DA7B008B0:
Child: parent = 0000010DA7B008B0, id = 1
Child: parent = 0000010DA7B008B0, id = 2
Child: parent = 0000010DA7B008B0, id = 3

child4: parent = 0000010DA7B008B0, id = 2
child5: parent = 0000000000000000, id = 1
Parent after removeChild(e1) call:
Parent =0000010DA7B008B0:
Child: parent = 0000010DA7B008B0, id = 2
Child: parent = 0000010DA7B008B0, id = 3

child6: parent = 0000000000000000, id = 2
Parent after removeChild(pChild4) call:
Parent =0000010DA7B008B0:
Child: parent = 0000010DA7B008B0, id = 3

pChild7: nullptr
pChild8: nullptr

A few 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 raw pointer 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 destructor must destroy all of the Child objects that the Parent object owns.
  5. Because there may be multiple pointers to the same Child object, it is unclear who has responsibility for deleting the object. This leads to memory leaks, and even worse, attempts to delete the same object multiple times. Ownership of a Child object can be indicated using owner<T> from the the Guideline Support Library but owner<T> documents and does not enforce ownership transfers. See the C++ Core Guidelines for information on owner<T>.

Pros

  1. With appropriate modifications to backport the code above, it may be used with C++98.
  2. Developers who have not kept up with recent C++ language changes are familiar with this programming idiom.

Cons

  1. Ownership of Child objects is not explicitly known. It is the coder’s responsibility to ensure that every Child object is deleted once and only once.

Implementing Parent-Child and Similar Relationships

I have been thinking a lot lately about relationships between objects. These relationships come in three varieties:

  1. One object acts as the “creator” and “destroyer” for other objects. In this case,  the “creator/destroyer” object does not need to keep track of the objects that it created, but the created objects must keep a reference to the “creator/destroyer” so that the objects can destroy themselves. The program must destroy the created objects before destroying the “creator/destroyer” object. One object, the parent, owns the other objects, the children, and must destroy its children objects before destroying itself.One example of this would be in an object-oriented implementation of the Vulkan API. Once a logical device object has been created, it can be used to create and destroy any number of other objects such as swap chains, render passes, pipelines, frame buffers, command pools, semaphores, and so forth. The logical device should not be destroyed until all of the created objects are destroyed. Otherwise, GPU memory leaks occur.
  2. In a parent-child relationship, the children may be created before or after the parent. Child objects may be added or removed from the parent as required. The parent must maintain a reference to each of the children it owns, and each child must maintain a reference to its parent. The program must destroy each non-owned object. One place that a parent-child relationship could be implemented is in Tasks and Task Lists. The Task List would be the parent, holding a number of Task children. Again, the Task List must not be destroyed until all of the Tasks that it “owns” are destroyed.
  3. An extension of the parent-child relationship is where each child could in turn be the parent of other child objects.This sort of relationship shows up in graphical user interface libraries. For example, a main window contains a number of child widgets, each of which may contain zero or more other widgets, which in turn may contain…

The creator- destroyer pattern can be thought of as a specialized case of the parent – child relationship with the parent (the creator – destroyer object) not keeping track of the children objects. This pattern will not be discussed further.

The next several posts will look at three ways of implementing parent – child relationships, and a single way of implementing the multiple level parent-child relationship.

Generalized Parent – Child Relationship

To begin, let’s look at the parent – child relationship:

class Parent {
public:
    Parent(/* args as needed */);
    ~Parent();
    void addChild(RefToChild1 child);
    RefToChild2 findChild(RefToChild1 child);
    RefToChild2 removeChild(RefToChild1 child);
...
private:
    vector m_children;
};

class Child {
public:
    Child(RefToParent parent, /* other args as needed */);
    RefToParent getParent();
    const ID getID();
...
private:
    RefToParent m_parent;
    ID m_id;
};

The next thtee posts will look at Parent and Child classes where RefToChild1, RefToChild2, and RefToParent are raw pointers, shared_ptrs and weak_ptrs, and finally, a combination of raw pointers, shared_ptrs and unique_ptrs. The pros and cons of each approach will be discussed.

Multi-Level Parent – Child Relationships

Following the Parent – Child posts, we will look at the pattern where each class contains a link to a parent object and links to any child objects. Only the shared_ptr implementation will be reviewed; however, a raw pointer and a combination of raw pointer, shared_ptr, and unique_ptr implementations could be developed.

No “code” is shown in this post; that is left for the detailed posts.

DoxyPress and Visual Studio

Last week I wrote about integrating Doxygen with Visual Studio. In one of the comments about that post, legalize suggested that I use its modern C++ replacement DoxyPress instead. DoxyPress is a fork of Doxygen so you should be able to migrate very easily from Doxygen to DoxyPress; there is functionality included to convert an existing Doxygen configuration file to a DoxyPress project file.

Installing DoxyPress

Here are the instructions for using DoxyPress with Visual Studio 2015 and 2017.

  1. Download the 32-bit or 64-bit Windows installer for DoxyPress as appropriate.
  2. Use Run as Administrator to execute the installer. If you do not run the installer as administrator, the installer will not be able to install the files into Program Files (or Program Files (x86) as appropriate). If you execute the installer on a computer running Windows 8/8.1 or 10, you will probably see the following messagebox. If you do not see this messagebox, you may skip forward to step 5.
    SmartScreen
    This is displayed because Microsoft is being cautious and warning you about applications and websites it does not recognize. It does not mean that the installer contains malware, just that it is not in Microsoft’s list of high use applications.
  3. To proceed, click on More info:
    SmartScreen-moreinfo
  4. Either heed the warning and click on the Don’t run button, in which case you cannot install DoxyPress, or click on the Run anyway button to continue.
    Note: I cannot and will not guarantee that the installer for any version of DoxyPress does not contain any malware. As with every application you install, you ultimately take responsibility for that.
  5. The DoxyPress installer dialog will be displayed:
    DoxyPressLanguage
    Choose your preferred language and click Next>.
  6. Continue clicking the Next> button until the installer installs DoxyPress. If the following messagebox is not displayed, proceed to step 7. If the messagebox is displayed, you did not use Run as Administrator.
    DoxyPressNoInstall
    Click the OK button to go back to the installer dialog and click Cancel, then go back to step 2 to rerun the installer using Run as Administrator.
  7. When the installation completes, click on the Finish button to close the installer.

Integrating DoxyPress With Visual Studio

  1. To integrate DoxyPress with Visual Studio, open Visual Studio and select the Tools -> External Tools… menu button. This will open the External Tools dialog:
    ExternalTools
  2. Click on the Add button. The dialog changes to:
    AddExternalTool
  3. Set the Title to DoxyPress, the Command to the executable for DoxyPress (e.g. C:\Program Files\DoxyPress\doxypress.exe), the Arguments to $(ProjectDir)DoxyPress.json, and the Initial directory to $(ProjectDir). Note that there is no ‘\’ character in the Arguments value. Check the Use Output window checkbox. The bottom portion of the dialog box should look similar to this:
    AddDoxyPress
  4. Click the Apply button to add DoxyPress as a menu item.
  5. Again click the Add button.
  6. Set the Title to DoxyPressApp, the Command to the executable for DoxyPressApp (e.g. C:\Program Files\DoxyPress\DoxyPressApp.exe), leave the Arguments blank, and set the Initial directory to $(ProjectDir). Do not check any checkboxes that are not already checked.
  7. Click the OK button to add the DoxyPressApp menu item and close the dialog box.

Generating Documentation the First Time

These instructions assume you have added the appropriate DoxyPress comments to your source code. To use DoxyPress to generate documentation:

  1. In Visual Studio, select the project you are documenting in the Solution Explorer.
  2. If this is the first time you will be generating the documenation for this project, select the Tools -> DoxyPressApp menu item. If this is first time you have ever run DoxyPressApp, the following messagebox will be displayed:DoxyPressAppSetupFileMissingThe setup file contains the location and size of the DoxyPressApp window, a list of the most recently opened DoxyPress project files, and the path to the directory that the last DoxyPress.json file was saved in.
  3. Click on the Default Location button. This saves the settings in C:\Users\<yourusername>\AppData\Local\CS\DoxyPressApp\DoxyPressApp.json. The DoxyPressApp dialog will now open:DoxyPressApp
  4. Select each of the topics in the Setup, Build Settings, and Output Formats tabs to configure the output that will be generated by DoxyPress. The various settings are described in the DoxyPress Project File web page.
  5. When done, select the Run tab:DoxyPressAppRun
  6. Click on the Options for DoxyPress button. This opens the Passed Parameters dialog:NamedParameters
  7. Enter the values you want, then click the Ok button to close the dialog.
  8. Click the Run DoxyPress button. The first time you click on Run DoxyPress, the Save File dialog will open so you can save the DoxyPress project file. Navigate to the project directory (the directory containing the Visual Studio project file (vcxproj file) and enter the file name DoxyPress.json. This file name must match the name specified in the Arguments in step 3 in the section Integrating DoxyPress With Visual Studio, above. Click the Save button to save the file and close the Save File dialog.
  9.  Your documentation will now be generated. If you selected HTML output in the Output Formats tab, click the Display HTML button to view the generated documentation; otherwise, open the generated documentation to ensure that it is created as you would like it.
  10. If you wish to make changes to how the documentation is generated, repeat steps 4 – 9 until you are satisfied. Once you are satisfied with the generated output, close DoxyPressApp.

Regenerating Documentation

When you want to regenerate your documentation, select the project in Visual Studio’s Solution Explorer and then select the Tools -> DoxyPress menu item. It will use the DoxyPress.json file you saved in the instructions above so it is not necessary to run DoxyPressApp again for the selected project.

Doxygen and Visual Studio

Doxygen is a tool for generating documentation from annotated source code. Originally created specifically for C++, it now also supports C, Objective-C, C#, PHP, Java, Python, IDL, Fortran, VHDL, Tcl, and D. Output formats include HTML, Latex, RTF (MS-Word), PostScript, hyperlinked PDF, compressed HTML, and Unix man pages. Although developed in OS X and Linux, there is also an MS Windows executable.

This post will not discuss how to document your source code for use with Doxygen, nor will it list the advantages and disadvantages of using Doxygen. You will have to decide if Doxygen is the right tool for you. You should see the Doxygen website for that. This post will simply show how to use Doxygen with Visual Studio.

There are no extensions for integrating Doxygen with Visual Studio. However, Doxygen, and Doxywizard, a wizard-based executable for creating the configuration file for use with Doxygen, are command line executables which can easily be run from the Visual Studio Tools menu. Adding Doxygen and Doxywizard to the Tools menu is done as follows. The instructions work for both Visual Studio 2015 and Visual Studio 2017.

  1. Download and install the latest Doxygen Windows binary.
  2. Open the Visual Studio Tools dropdown menu and select External Tools…
  3. This opens the External Tools dialog:
    ExternalTools
    Click the Add button. The dialog changes to this:
    AddExternalTool
  4. Change the Title to Doxygen, the command to point to the Doxygen executable (C:\Program Files\doxygen\bin\doxygen.exe on my computer), the arguments to $(ProjectDir)\Doxyfile, and initial directory to $(ProjectDir). Check the Use Output window checkbox. The lower portion of the dialog box will look like this:
    AddDoxygen
    $(ProjectDir) is the macro in Visual Studio that points to the project directory (the directory that contains the project’s vcxproj file).
  5. Click the Apply button to add the Doxygen menu item to the Tools menu.
  6. Click the Add button.
  7. Enter DoxyWizard as the Title, the location of the doxywizard executable  as the Command (e.g. C:\Program Files\doxygen\bin\doxywizard.exe), leave Arguments blank, and $(ProjectDir) as initial directory. Leave all checkboxes unchecked.
  8. Click the OK button to add the DoxyWizard menu item and close the dialog box.

The first time you use Doxygen with a project, select the Tools -> DoxyWizard menu item to open the DoxyWizard dialog, shown here:
DoxyWizardDialog

  1. Step 1: Set the working directory to be the $(ProjectDir) directory. That is the directory containing the project’s vcxproj file.
  2. Step2: Use the Wizard and/or Expert tabs to set the configuration values.
  3. Select the Run tab and then Run doxygen.
  4. If you wish, go back and change the various configuration values.
  5. Once you are satisfied with the values you have set, close the dialog. This will display the Unsaved changes message box. Click Save to save the configuration. Select the directory that contains the project’s vcxproj file. This will ensure that the configuration file is found when you run Doxygen.

You should only run DoxyWizard once for each project that you are documenting. For each subsequent document generation, use Tools -> Doxygen. Provided you saved the configuration file in the correct location, Doxygen will run correctly, saving its generated documentation to the specified directory, and sending its output to the Visual Studio Output window.

 

More on Naked Primitives

The post Strong Typing or Naked Primitives showed an example of problems that can occur when using common variable types (or if you prefer, built-in data types) as argument types in methods and functions, and a potential solution. The post discussed a Size struct with the constructor:

struct Size {
    Size(const uint32_t width, const uint32_t height);
};

and the possible problems that can occur, such as placing the height argument before the width argument.

User Defined Literals continued the example by showing how to use and convert different unit types in the arguments. Specifically, the best way to supply the width and height arguments in units of pixels, inches, and centimetres.

This post will continue to investigate ways of specifying arguments to remove the confusion that can occur when using common variable types as arguments.

Character String to Enumeration

In this post, we will declare a class that partially encapsulates the functionality of C File I/O. A good introduction to C File I/O is provided by programiz.com.

Here is a simple first File class and how to use it. We are interested only in the arguments; the actual implementation is not included. That is up to you to provide.

class File
{
public:
    File(const char* fileName, const char* mode) 
        { /* create file if necessary, and open it */}
    ~File() { /* close file */}
    void print(const char* charString, bool appendReturn) 
        { /* print charsString */}
};

int main()
{
    File file1("file1", "a");
    file1.print("A line", true);
    File file2("file2", "w");
    file2.print("A line, no return", false);
    File file3("file3", "xmf_");
    return 0;
}

The constructor for file1 will create the file if it does not exist, then move the cursor for writing to the file to past the last character in the file. The constructor for file2 will create the file if it does not exist, or erase the contents of the file if it does exist, and place the write cursor at the beginning of the file. The constructor for file3 contains invalid characters in the mode field, so you would have to add code to the File constructor to handle this.

Another problem with the constructor as it is declared is that there are two arguments of type const char* so if you specify the arguments in the wrong order when calling the constructor, the compiler will not catch the error. We will not look at this further as this has already been discussed in Strong Typing or Naked Primitives.

So, our only concern here is the second argument, the mode. Since there are a limited number of values, this appears to be a good candidate for an enumeration instead of a string. Looking at the possible values for mode you will notice that it serves two different purposes:

  1. Indicates that the file should be opened for some combination of reading, writing, or appending.
  2. Indicates whether the file contents are text or binary.

To simplify the code in the constructor, let’s use two separate enumerations, one for the operation type, and one for the contents type. Here is the code resulting from these changes:

enum Mode {
    eRead = 1,
    eWrite = 2,
    eAppend = 4
};

enum Type {
    eText = 1,
    eBinary = 2
};

class File
{
public:
    File(const char* fileName, unsigned int mode, enum Type type) 
        { /* create file if necessary, and open it */}
    ~File() { /* close file */}
    void print(const char* charString, bool appendReturn) 
        { /* print charString */}
};

int main()
{
    File file1("file1", eAppend, eText);
    file1.print("A line", true);
    File file2("file2", eWrite, eText);
    file2.print("A line, no return", false);
    File file3("file3", eRead | eWrite, eText);
    File file4("file4", eBinary | 16, eText);
    return 0;
}

The Mode enumeration contains only three values: eRead, eWrite, and eAppend.  No attempt has been made to distinguish between the fopen modes of “w+” and “r+”. That is left as an exercise for the reader because it is not germane to the topic of this post.

Since a file can be opened for both reading and writing, or reading and appending, the second argument for the File constructor is specified as unsigned int. Multiple values of mode can therefore be OR’ed together. See, for example, the constructor call for file3. Everything looks good so far. Now look at the constructor call for file4. Here the Mode has been set to a combination of eBinary, which is a Type not a Mode, and 16 which has no numerical equivalent in Mode.

One potential solution to this problem is to add two enumeration values to the Mode enumeration: eReadAndWrite, and eReadAndAppend, and to change the second argument type in the File constructor to enum Mode. This works in this case, but what if Mode had a large number of individual values which could be combined in many different ways? Defining a value for every combination would not be a viable option.

The solution is to change the Mode and Type enumerations to both be enum class and to add a class that can OR together multiple Mode values:

enum class Mode : unsigned int {
    eRead = 1,
    eWrite = 2,
    eAppend = 4
};

enum class Type : unsigned int {
    eText = 1,
    eBinary = 2
};

template <typename BitType, typename MaskType = unsigned int>
class Flags
{
public:
 Flags()
 : m_mask(0) {}

 Flags(BitType bit)
 : m_mask(bit) {}

 Flags(Flags<BitType> const& rhs)
 : m_mask(rhs.m_mask) {}

 Flags<BitType> operator|(Flags<BitType> const& rhs) const
 {
 Flags<BitType, MaskType> result(*this);
 result |= rhs;
 return result;
 }

private:
 MaskType m_mask;
};

using ModeFlags = Flags<Mode>;
ModeFlags operator|(Mode bit0, Mode bit1)
{
 return ModeFlags(bit0) | bit1;
}


class File
{
public:
    File(const char* fileName, ModeFlags mode, Type type) 
    { /* create file if necessary, and open it */}
    ~File() { /* close file */}
    void print(const char* charString, bool appendReturn) 
    { /* print charString */}
};

int main()
{
    File file1("file1", Mode::eAppend, Type::eText);
    file1.print("A line", true);
    File file2("file2", Mode::eWrite, Type::eText);
    file2.print("A line, no return", false);
    File file3("file3", Mode::eRead | Mode::eWrite, Type::eText);
    File file4("file4", Type::eBinary | 16, Type::eText);
    return 0;
}

The Flags class has been borrowed from the Vulkan C++ bindings, vulkan.hpp, available as part of the Vulkan SDK, or separately from its GitHub repository. I have included only those parts of the class that are required for this example to compile or not where required. Things to note:

  1. The second argument in the File constructor has been changed to type ModeFlags, which is just an alias for Flags<Mode>.
  2. The line containing the constructor call for file3 now compiles.
  3. The line containing the constructor call for file4 does not compile because Type::eBinary and 16 are both not of type Mode. But, since this is a programming error, that is what we want to happen.

Update (February 17, 2017): A few days after publishing this post, I ran across Alternative to select-many bitmask that discusses a number of methods for combining bitmask bits.

Boolean to Enumeration

In the section above, we changed a character string that could contain a limited number of values to enumerations. By doing so, we ensured that invalid values could not be coded, and we ensured that 6 months from now when you or someone else looks at the code, they will not have to reference the documentation or the class’s declaration to determine the meaning of each argument.

Now we move on to boolean arguments. In the main function in the examples above, there are calls to File::print. The second argument in these calls contains either true or false. Can you tell what those argument values mean without looking at the class declaration? What happens if you change the value of this argument to 6? Hint: the program will still compile, but the compiler will generate a warning.

Let’s change this boolean to an enum class.

enum class LineEnd : bool {
    eNoReturn = false,
    eReturn = true
};
.
.
.
    void print(const char* charString, LineEnd appendReturn) 
    { /* print charString */}
.
.
.
    file1.print("A line", LineEnd::eReturn);
    file2.print("A line, no return", LineEnd::eNoReturn);

Now there is no confusion as to the meaning of the second argument to the File::print method. Also, trying to use values like true or 6 for this argument cause a compiler error.

Update (February 17, 2017): A few days after I published this post, Andrzej Krzemieński published a post with an alternative that uses a tagged_bool class.

Conclusions

The examples in this post are contrived to illustrate the points I am trying to make. I do not expect that anyone would actually try to write the File class I have started to declare because the functionality that would be provided in such a class is already available in classes in the standard C++ library. However, a number of conclusions can be drawn from using the techniques shown in these examples:

  1. By changing function and method arguments from common variable types to classes, including enum classes, a number of errors that formerly would only show up at program execution time can now be caught at compile time. This moves the burden of dealing with these errors from the user to the developer where they belong.
  2. Assuming that the classes and enumeration values are properly named, the meaning of the argument values specified  in the source code are much clearer, thereby making the work of the code maintainer much easier.