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.