5
\$\begingroup\$

I am trying to port a TreeBuilder Python class to C++, keeping the structure as close as possible to the original. Here is a simplified Python version: https://onlinegdb.com/I4dg0hCtg

  • The purpose of tree is to represent a generic game tree (Chess, Go, Checkers, etc.) It is passed forward to a class (Whatever), which builds an another representation based on some depth-wise statistics.
  • Environment here is a mock class. In the real application, actions are passed back based on the game's rules
  • The main reason of porting the code is speed. I am very new to C++, but as I know dynamic allocation on the heap costs more. I do not know if it worth (or even possible) to build such a tree without dynamic initialization.
  • As far as I know using raw pointers has a little benefit of performance over smart pointers, but GC should be done by hand. My plan is after the trees are generated, and used by Whatever class (see my first point), Whatever class has to destroy the tree's node recursively. (Node's destructor should delete its children)

main.cc

#include "Node.h"
#include "Environment.h"
#include "Options.h"
#include "TreeBuilder.h"

int main() {
    Node* node = new Node();
    Environment* environment = new Environment();
    node->environment = environment;
    Options* options = new Options();
    options->root = node;

    TreeBuilder* treeBuilder = new TreeBuilder();
    Node* tree = treeBuilder->buildTree(options);

    tree->printTree();
    return 0;
}

Node.h

#pragma once
#include <vector>
#include <iostream>

// Environment and Node has circular dependency, forward declaration
class Environment;

class Node
{
public:
  Node * parent = nullptr;
  int depth = 0;
  std::vector < Node * >children;
  Environment *environment = nullptr;

  void printTree ();
  ~Node();
};

Node.cc

#include "Node.h"

void Node::printTree ()
{
  // Print this node's depth
  for (int i = 0; i < this->depth; i++)
  {
    std::cout << "\t";
  }

  std::cout << "- Depth: " << this->depth << std::endl;

  // Recursively print the children
  for (Node * child:this->children) {
    child->printTree ();
  }
}

Node::~Node() {
  for (Node* child : children) {
    delete child;
  }
}

Environment.h

#pragma once

#include <vector>
#include "Node.h"

class Environment
{
public:
  std::vector < int >getActions (Node * parent);
};

Environment.cc

#include "Environment.h"

std::vector<int> Environment::getActions(Node* parent) {
    std::vector<int> actions;
    if (parent->depth != 3) {
        for (int x = 0; x <= parent->depth; x++) {
            actions.push_back(x * 100);
        }
    }
    return actions;
}

Options.h

#pragma once

#include "Node.h"
#include "Environment.h"

class Options
{
public:
  Node * root = nullptr;
  Environment *environment = nullptr;
};

TreeBuilder.h

#pragma once

#include "Node.h"
#include "Environment.h"
#include "Options.h"

class TreeBuilder
{
public:
  Environment * environment = nullptr;
  std::vector < Node * >getChildren (Node * parent);
  void _buildTree (Node * node);
  Node *buildTree (Options * options);
};

TreeBuilder.cc

#include "TreeBuilder.h"
#include <iostream>

std::vector < Node * >TreeBuilder::getChildren (Node * parent)
{
  std::vector < Node * >children;
  std::vector < int >
    actions = this->environment->getActions (parent);

  for (int x = 0; x < actions.size (); x++)
    {
      Node* node = new Node ();
      node->parent = parent;
      node->depth = parent->depth + 1;
      node->environment = parent->environment;
      children.push_back (node);
    }
  return children;
}

void
TreeBuilder::_buildTree (Node * node)
{
  std::vector < Node * >children = this->getChildren (node);
  node->children = children;
  for (int i = 0; i < children.size (); i++)
    {
      this->_buildTree (children[i]);
    }
}

Node* TreeBuilder::buildTree (Options * options)
{
  Node *root = new Node ();
  root->environment = options->root->environment;
  this->environment = root->environment;
  this->_buildTree (root);
  return root;
}

Here is a playground version, https://onlinegdb.com/eHbJGXqN4, to see it in action.

\$\endgroup\$
3
  • \$\begingroup\$ It would be helpful to know if you are trying to implement a B tree or a binary tree. \$\endgroup\$ Commented Apr 12, 2023 at 14:02
  • \$\begingroup\$ @pacmaninbw not a binary tree. E.g.: chess' root Node (starting position) has 8x2 (pawns) + 2x2 (knights) = 20 children Nodes (legal opening moves) \$\endgroup\$ Commented Apr 12, 2023 at 14:30
  • \$\begingroup\$ Rather than using on online compiler download free development software. If you are using Linux use the gcc or g++ compiler. If you are on Windows down load Cygwin and develop in a Linux environment. \$\endgroup\$ Commented Apr 12, 2023 at 16:44

1 Answer 1

7
\$\begingroup\$

Initial usage

This is not C++ like. You are using dynamic allocation where it is not needed. Simply make things local variables. Also use the constructor to make sure the objects are correctly initialized on construction (don't use a two step initialization of create then set up).

The tree you eventually build will have to be dynamic but you should use smart pointers to manage the nodes. There is absolutely zero cost of std::unique_ptr over a RAW pointer it simply makes sure the memory is managed correctly (and release is much more well-defined compared to Garbage collected languages (think of it as fine grain garbage collection that happens immediately when the object is no longer needed)).

This is how I would have the main function look.

#include "Node.h"
#include "Environment.h"
#include "Options.h"
#include "TreeBuilder.h"

int main()
{
    Environment  environment;
    Node         node(environment);
    Options      options(node);

    TreeBuilder  treeBuilder;
    
    std::unique_ptr<Node>  tree = treeBuilder.buildTree(options);

    tree->printTree();
}

Statements:

As far as I know using raw pointers has a little benefit of performance over smart pointers, but GC should be done by hand.

There are no benifits to using RAW pointers.

GC should NOT be done by hand (this is not C).
Use smart pointers or containers to handle resource management.

There is no cost of std::unique_ptr over a RAW pointer. BUT lots of benifits.

My plan is after the trees are generated, and used by Whatever class (see my first point), Whatever class has to destroy the tree's node recursively. (Node's destructor should delete its children)

I don't think it will be a problem.
Here you are making a Node a container (its responsible for resource managment). This does mean you need to know the rule of 3/5 to make sure you do resource management correctly.

Code Review

Hate this:

#pragma once

It is non-standard.
Use include guards. It's standard and works as expected everywhere.

#ifndef   MY_NAMESPACE_MY_CLASS_H
#define   MY_NAMESPACE_MY_CLASS_H
// STUFF
#endif

Don't see streams being used in Node.h

#include <iostream>. //remove this.

// Environment and Node has circular dependency, forward declaration

Unless you explicitly use a class in a header file, or its size is needed then you should always use forward declaration.

class Environment;

why are all the members public?

Data members should always be private (unless you are making a simply property bag).

class Node
{
public:
  Node * parent = nullptr;
  int depth = 0;
  std::vector < Node * >children;
  Environment *environment = nullptr;

  void printTree (); // only for debugging
};

Personally. Assuing that the Node always needs an environment I would make that a reference (not a pointer).

But either way I would allow you to pass the environment as a parameter in the constructor (see main description above).


You have not obeyed the rule of three.

std::vector < Node * >children;

If your class does resource management (which it does with children). Then you need to make sure you either define the copy semantics or disable them. Otherwise you are going to run into issues.

I vote for diabling at this point as it is simpler:

Node(Node const&)            = delete;
Node& operator=(Node const&) = delete;

Some Style pointers:

Types are very important in C++. So all the type information is usually grouped together away from the name.

  Node * parent = nullptr;

Normally I would put the * with the Node like this:

  Node*     parent = nullptr;

Not fond of the extra white space here:

std::vector < Node * >children;

I would write it like this:

std::vector<Node*>     children;

Environment.h

You don't use Node or need the Node size in the Environment.h header. So don't include the header file.

#include "Node.h"

Just forward declare the Node inside the header.
You may have to include it inside the source file (but try not to include it in the header file).


Again public Data Members is a bad idea.

class Environment
{
public:
  std::vector < int >getActions (Node * parent);
};

Options.h

You don't use the Node or Environment objects or depend on their size. So use forward declaration inside the header file.

#include "Node.h"
#include "Environment.h"

Not sure. But if either of these is required then they should be references not pointers. Add a constructor that allows you to initialize the references on construction.

class Options
{
public:
  Node * root = nullptr;
  Environment *environment = nullptr;
};

I see a couple of places where you use this->.

STOP THAT.

If you need to use this-> it means you have done a bad job naming something and you need to explicitly make sure you are using the member. This is a sure way to have bugs in your code.

Make sure all objects are uniquely defined and don't shadow other variables. Also make sure the names are meaningful so you can quickly identify that you are using the correct variable.


\$\endgroup\$
2
  • \$\begingroup\$ Could you elaborate on your statement: Just forward declare the Node inside the header? \$\endgroup\$ Commented Apr 26, 2023 at 13:57
  • 1
    \$\begingroup\$ @MartinSand Rather than #include "Node.h" prefer to use class Node;. If you don't need to know the size of Node and you usually don't use members in the header file so you don't need to actually include the header files in header files. \$\endgroup\$ Commented Apr 26, 2023 at 15:43

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.