Recursive Quadtree implementation in C++
Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
6
down vote
favorite
I've been working on a recursive quadtree in C++. How can I improve my code? My primary goals are improving the performance/speed, as well as practice.
QuadTree.h
#pragma once
#include <vector>
#include <algorithm>
#include <functional>
struct AABB
double left, top, right, bottom;
AABB()
AABB(const double &l, const double &t, const double &r, const double &b);
bool within(const AABB&) const;
bool intersects(const AABB&) const;
;
struct Object
friend class QuadTree;
public:
AABB _bound;
Object() ;
Object(const AABB&, void *data = nullptr);
void setData(void *data);
void *getData() const;
private:
void *_data = nullptr;
QuadTree *_qt = nullptr;
;
class QuadTree
public:
QuadTree(const AABB&, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level = 0, QuadTree *parent = nullptr);
bool insert(Object*);
bool remove(Object*);
void update(Object*);
bool contains(Object*) const;
void search(const AABB&, const std::function<void(Object*)> &callback) const;
void query(const AABB&, std::vector<Object*> &returnObjects) const;
bool any(const AABB&, const std::function<bool(Object*)> &condition = nullptr) const;
unsigned getTotalChildren() const;
unsigned getTotalObjects() const;
void clear();
QuadTree()
~QuadTree();
private:
AABB _bound;
QuadTree* _parent;
QuadTree* _children[4];
bool _isLeaf = true;
double _centerX;
double _centerY;
unsigned _level;
unsigned _maxLevel;
unsigned _capacity;
std::vector<Object*> _objects;
void subdivide();
void discardEmptyBuckets(QuadTree *node);
QuadTree *getChild(const AABB&) const;
;
QuadTree.cpp
#include "QuadTree.h"
//** AABB **//
AABB::AABB(const double &l, const double &t, const double &r, const double &b) :
left(l),
top(t),
right(r),
bottom(b)
bool AABB::within(const AABB &bound) const
return left > bound.left && bottom > bound.bottom
&& right < bound.right && top < bound.top;
bool AABB::intersects(const AABB &bound) const
if (bound.right <= left) return false;
if (bound.left >= right) return false;
if (bound.top <= bottom) return false;
if (bound.bottom >= top) return false;
return true; // intersection
//** Object **//
Object::Object(const AABB &bound, void *data):
_bound(bound),
_data(data)
;
void Object::setData(void *data)
_data = data;
void *Object::getData() const
return _data;
//** QuadTree **//
QuadTree::QuadTree(const AABB &bound, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level, QuadTree *parent):
_capacity(capacity),
_maxLevel(maxLevel),
_level(level),
_parent(parent),
_centerX((bound.left + bound.right) * 0.5f),
_centerY((bound.bottom + bound.top) * 0.5f),
_bound(bound)
// Inserts an object into this quadtree
bool QuadTree::insert(Object *obj)
// Item already exists
if (obj->_qt != nullptr)
return false;
// insert object at the lowest level
if (!_isLeaf)
if (QuadTree *child = getChild(obj->_bound))
return child->insert(obj);
_objects.push_back(obj);
obj->_qt = this; // used for quick search by quadtree
// Subdivide if required
if (_isLeaf && _level < _maxLevel && _objects.size() >= _capacity)
subdivide();
// Re-insert objects into new quadrant
for (unsigned i = 0; i < _objects.size();)
Object *object = _objects[i];
if (QuadTree *child = getChild(object->_bound))
_objects.erase(_objects.begin() + i);
object->_qt = nullptr;
child->insert(object);
else ++i;
return true;
// Removes an object from this quadtree
bool QuadTree::remove(Object *obj)
if (obj->_qt == nullptr)
return false;
if (obj->_qt != this)
return obj->_qt->remove(obj);
_objects.erase(std::find(_objects.begin(), _objects.end(), obj));
obj->_qt = nullptr;
discardEmptyBuckets(this);
return true;
// Removes and re-inserts object into quadtree (for objects that move)
void QuadTree::update(Object *obj)
// Checks if object exists in this quadtree
bool QuadTree::contains(Object *obj) const
if (obj->_qt == nullptr)
return false;
if (obj->_qt != this)
return obj->_qt->contains(obj);
return std::find(_objects.begin(), _objects.end(), obj) != _objects.end();
// Searches quadtree for objects within the provided boundary and returns them in callback
void QuadTree::search(const AABB &bound, const std::function<void(Object*)> &callback) const
// Search children first
if (!_isLeaf)
if (QuadTree *child = getChild(bound))
child->search(bound, callback);
else
for (auto&& node : _children)
if (node->_bound.intersects(bound))
node->search(bound, callback);
// Now search objects
for (auto&& obj : _objects)
if (obj->_bound.intersects(bound))
callback(obj);
// Searches quadtree for objects within the provided boundary and adds them to provided vector
void QuadTree::query(const AABB &bound, std::vector<Object*> &returnObjects) const
search(bound, [&returnObjects](Object *obj)
returnObjects.push_back(obj);
);
// Checks if any object exists in the specified bounds (with optional filter)
bool QuadTree::any(const AABB &bound, const std::function<bool(Object*)> &condition) const
bool found = false;
search(bound, [&condition, &found](Object *obj) );
return found;
// Returns total children count for this quadtree
unsigned QuadTree::getTotalChildren() const
unsigned count = 0;
if (!_isLeaf)
for (auto&& child : _children)
count += child->getTotalChildren();
return (_isLeaf? 0 : 4) + count;
// Returns total object count for this quadtree
unsigned QuadTree::getTotalObjects() const
unsigned count = 0;
if (!_isLeaf)
for (auto&& node : _children)
count += node->getTotalObjects();
return _objects.size() + count;
// Removes all objects and children from this quadtree
void QuadTree::clear()
if (!_objects.empty())
for (auto&& obj : _objects)
remove(obj);
_objects.clear();
if (!_isLeaf)
delete _children[0];
delete _children[1];
delete _children[2];
delete _children[3];
_isLeaf = true;
// Subdivides into four quadrants
void QuadTree::subdivide()
// Bottom right
_children[0] = new QuadTree(
_centerX, _centerY, _bound.right, _bound.bottom ,
_capacity, _maxLevel, _level + 1, this
);
// Bottom left
_children[1] = new QuadTree(
_bound.left, _centerY, _centerX, _bound.bottom ,
_capacity, _maxLevel, _level + 1, this
);
// Top left
_children[2] = new QuadTree(
_bound.left, _bound.top, _centerX, _centerY ,
_capacity, _maxLevel, _level + 1, this
);
// Top right
_children[3] = new QuadTree(
_centerX, _bound.top, _bound.right, _centerY ,
_capacity, _maxLevel, _level + 1, this
);
_isLeaf = false;
// Discards buckets if all children are leaves and contain no objects
void QuadTree::discardEmptyBuckets(QuadTree *node)
if (!node->_objects.empty())
return;
if (!node->_isLeaf)
for (auto &&child : node->_children)
if (!child->_isLeaf
node->clear();
if (node->_parent != nullptr)
discardEmptyBuckets(node->_parent);
// Returns child/quadrant that the provided boundary is in
QuadTree *QuadTree::getChild(const AABB &bound) const
bool bottom = bound.bottom > _centerY;
bool left = bound.left < _centerX;
bool top = !bottom && bound.top < _centerY;
if (left && bound.right < _centerX)
if (top) return _children[1]; // top left
if (bottom) return _children[2]; // bottom left
else if (!left)
if (top) return _children[0]; // top right
if (bottom) return _children[3]; // bottom right
return nullptr;
QuadTree::~QuadTree()
clear();
I've done some profiling in VS2017 and I have found that the insert
method takes the longest (i'm guessing due to the recursion), followed by AABB::intersects
, then search
. I have no clue on what I can do to make those methods faster. I'm curious to see what you guys have to say about it :)
c++ performance c++11 recursion tree
add a comment |Â
up vote
6
down vote
favorite
I've been working on a recursive quadtree in C++. How can I improve my code? My primary goals are improving the performance/speed, as well as practice.
QuadTree.h
#pragma once
#include <vector>
#include <algorithm>
#include <functional>
struct AABB
double left, top, right, bottom;
AABB()
AABB(const double &l, const double &t, const double &r, const double &b);
bool within(const AABB&) const;
bool intersects(const AABB&) const;
;
struct Object
friend class QuadTree;
public:
AABB _bound;
Object() ;
Object(const AABB&, void *data = nullptr);
void setData(void *data);
void *getData() const;
private:
void *_data = nullptr;
QuadTree *_qt = nullptr;
;
class QuadTree
public:
QuadTree(const AABB&, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level = 0, QuadTree *parent = nullptr);
bool insert(Object*);
bool remove(Object*);
void update(Object*);
bool contains(Object*) const;
void search(const AABB&, const std::function<void(Object*)> &callback) const;
void query(const AABB&, std::vector<Object*> &returnObjects) const;
bool any(const AABB&, const std::function<bool(Object*)> &condition = nullptr) const;
unsigned getTotalChildren() const;
unsigned getTotalObjects() const;
void clear();
QuadTree()
~QuadTree();
private:
AABB _bound;
QuadTree* _parent;
QuadTree* _children[4];
bool _isLeaf = true;
double _centerX;
double _centerY;
unsigned _level;
unsigned _maxLevel;
unsigned _capacity;
std::vector<Object*> _objects;
void subdivide();
void discardEmptyBuckets(QuadTree *node);
QuadTree *getChild(const AABB&) const;
;
QuadTree.cpp
#include "QuadTree.h"
//** AABB **//
AABB::AABB(const double &l, const double &t, const double &r, const double &b) :
left(l),
top(t),
right(r),
bottom(b)
bool AABB::within(const AABB &bound) const
return left > bound.left && bottom > bound.bottom
&& right < bound.right && top < bound.top;
bool AABB::intersects(const AABB &bound) const
if (bound.right <= left) return false;
if (bound.left >= right) return false;
if (bound.top <= bottom) return false;
if (bound.bottom >= top) return false;
return true; // intersection
//** Object **//
Object::Object(const AABB &bound, void *data):
_bound(bound),
_data(data)
;
void Object::setData(void *data)
_data = data;
void *Object::getData() const
return _data;
//** QuadTree **//
QuadTree::QuadTree(const AABB &bound, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level, QuadTree *parent):
_capacity(capacity),
_maxLevel(maxLevel),
_level(level),
_parent(parent),
_centerX((bound.left + bound.right) * 0.5f),
_centerY((bound.bottom + bound.top) * 0.5f),
_bound(bound)
// Inserts an object into this quadtree
bool QuadTree::insert(Object *obj)
// Item already exists
if (obj->_qt != nullptr)
return false;
// insert object at the lowest level
if (!_isLeaf)
if (QuadTree *child = getChild(obj->_bound))
return child->insert(obj);
_objects.push_back(obj);
obj->_qt = this; // used for quick search by quadtree
// Subdivide if required
if (_isLeaf && _level < _maxLevel && _objects.size() >= _capacity)
subdivide();
// Re-insert objects into new quadrant
for (unsigned i = 0; i < _objects.size();)
Object *object = _objects[i];
if (QuadTree *child = getChild(object->_bound))
_objects.erase(_objects.begin() + i);
object->_qt = nullptr;
child->insert(object);
else ++i;
return true;
// Removes an object from this quadtree
bool QuadTree::remove(Object *obj)
if (obj->_qt == nullptr)
return false;
if (obj->_qt != this)
return obj->_qt->remove(obj);
_objects.erase(std::find(_objects.begin(), _objects.end(), obj));
obj->_qt = nullptr;
discardEmptyBuckets(this);
return true;
// Removes and re-inserts object into quadtree (for objects that move)
void QuadTree::update(Object *obj)
// Checks if object exists in this quadtree
bool QuadTree::contains(Object *obj) const
if (obj->_qt == nullptr)
return false;
if (obj->_qt != this)
return obj->_qt->contains(obj);
return std::find(_objects.begin(), _objects.end(), obj) != _objects.end();
// Searches quadtree for objects within the provided boundary and returns them in callback
void QuadTree::search(const AABB &bound, const std::function<void(Object*)> &callback) const
// Search children first
if (!_isLeaf)
if (QuadTree *child = getChild(bound))
child->search(bound, callback);
else
for (auto&& node : _children)
if (node->_bound.intersects(bound))
node->search(bound, callback);
// Now search objects
for (auto&& obj : _objects)
if (obj->_bound.intersects(bound))
callback(obj);
// Searches quadtree for objects within the provided boundary and adds them to provided vector
void QuadTree::query(const AABB &bound, std::vector<Object*> &returnObjects) const
search(bound, [&returnObjects](Object *obj)
returnObjects.push_back(obj);
);
// Checks if any object exists in the specified bounds (with optional filter)
bool QuadTree::any(const AABB &bound, const std::function<bool(Object*)> &condition) const
bool found = false;
search(bound, [&condition, &found](Object *obj) );
return found;
// Returns total children count for this quadtree
unsigned QuadTree::getTotalChildren() const
unsigned count = 0;
if (!_isLeaf)
for (auto&& child : _children)
count += child->getTotalChildren();
return (_isLeaf? 0 : 4) + count;
// Returns total object count for this quadtree
unsigned QuadTree::getTotalObjects() const
unsigned count = 0;
if (!_isLeaf)
for (auto&& node : _children)
count += node->getTotalObjects();
return _objects.size() + count;
// Removes all objects and children from this quadtree
void QuadTree::clear()
if (!_objects.empty())
for (auto&& obj : _objects)
remove(obj);
_objects.clear();
if (!_isLeaf)
delete _children[0];
delete _children[1];
delete _children[2];
delete _children[3];
_isLeaf = true;
// Subdivides into four quadrants
void QuadTree::subdivide()
// Bottom right
_children[0] = new QuadTree(
_centerX, _centerY, _bound.right, _bound.bottom ,
_capacity, _maxLevel, _level + 1, this
);
// Bottom left
_children[1] = new QuadTree(
_bound.left, _centerY, _centerX, _bound.bottom ,
_capacity, _maxLevel, _level + 1, this
);
// Top left
_children[2] = new QuadTree(
_bound.left, _bound.top, _centerX, _centerY ,
_capacity, _maxLevel, _level + 1, this
);
// Top right
_children[3] = new QuadTree(
_centerX, _bound.top, _bound.right, _centerY ,
_capacity, _maxLevel, _level + 1, this
);
_isLeaf = false;
// Discards buckets if all children are leaves and contain no objects
void QuadTree::discardEmptyBuckets(QuadTree *node)
if (!node->_objects.empty())
return;
if (!node->_isLeaf)
for (auto &&child : node->_children)
if (!child->_isLeaf
node->clear();
if (node->_parent != nullptr)
discardEmptyBuckets(node->_parent);
// Returns child/quadrant that the provided boundary is in
QuadTree *QuadTree::getChild(const AABB &bound) const
bool bottom = bound.bottom > _centerY;
bool left = bound.left < _centerX;
bool top = !bottom && bound.top < _centerY;
if (left && bound.right < _centerX)
if (top) return _children[1]; // top left
if (bottom) return _children[2]; // bottom left
else if (!left)
if (top) return _children[0]; // top right
if (bottom) return _children[3]; // bottom right
return nullptr;
QuadTree::~QuadTree()
clear();
I've done some profiling in VS2017 and I have found that the insert
method takes the longest (i'm guessing due to the recursion), followed by AABB::intersects
, then search
. I have no clue on what I can do to make those methods faster. I'm curious to see what you guys have to say about it :)
c++ performance c++11 recursion tree
I'm a little confused reading this. If my root-levelQuadTree
has bounds from, say, -1 to +1 in both directions, it will be subdivided into 4 quads from 0 to ±1, etc. If I have an object that's centered at the origin, it looks to me like it will only end up in one of the second-level quads, not all 4, which is where it needs to be. Am I misunderstanding that?
â user1118321
May 21 at 3:00
It depends on the size of the object. It will only be re-inserted into one of the second-level quadrants if its boundaries can fit inside of one (aka ifgetChild
does not return nullptr), otherwise, it is skipped and remains inside all four quadrants. I should probably make that more clear :P
â m-byte
May 21 at 3:51
remember to upvote helpful answers!
â JDà Âugosz
May 23 at 0:46
What are you using this quadtree for ? collision detection, finding closest pairs ??
â Harald Scheirich
May 23 at 1:01
@HaraldScheirich I'm using it mainly for collision detection.
â m-byte
May 23 at 1:40
add a comment |Â
up vote
6
down vote
favorite
up vote
6
down vote
favorite
I've been working on a recursive quadtree in C++. How can I improve my code? My primary goals are improving the performance/speed, as well as practice.
QuadTree.h
#pragma once
#include <vector>
#include <algorithm>
#include <functional>
struct AABB
double left, top, right, bottom;
AABB()
AABB(const double &l, const double &t, const double &r, const double &b);
bool within(const AABB&) const;
bool intersects(const AABB&) const;
;
struct Object
friend class QuadTree;
public:
AABB _bound;
Object() ;
Object(const AABB&, void *data = nullptr);
void setData(void *data);
void *getData() const;
private:
void *_data = nullptr;
QuadTree *_qt = nullptr;
;
class QuadTree
public:
QuadTree(const AABB&, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level = 0, QuadTree *parent = nullptr);
bool insert(Object*);
bool remove(Object*);
void update(Object*);
bool contains(Object*) const;
void search(const AABB&, const std::function<void(Object*)> &callback) const;
void query(const AABB&, std::vector<Object*> &returnObjects) const;
bool any(const AABB&, const std::function<bool(Object*)> &condition = nullptr) const;
unsigned getTotalChildren() const;
unsigned getTotalObjects() const;
void clear();
QuadTree()
~QuadTree();
private:
AABB _bound;
QuadTree* _parent;
QuadTree* _children[4];
bool _isLeaf = true;
double _centerX;
double _centerY;
unsigned _level;
unsigned _maxLevel;
unsigned _capacity;
std::vector<Object*> _objects;
void subdivide();
void discardEmptyBuckets(QuadTree *node);
QuadTree *getChild(const AABB&) const;
;
QuadTree.cpp
#include "QuadTree.h"
//** AABB **//
AABB::AABB(const double &l, const double &t, const double &r, const double &b) :
left(l),
top(t),
right(r),
bottom(b)
bool AABB::within(const AABB &bound) const
return left > bound.left && bottom > bound.bottom
&& right < bound.right && top < bound.top;
bool AABB::intersects(const AABB &bound) const
if (bound.right <= left) return false;
if (bound.left >= right) return false;
if (bound.top <= bottom) return false;
if (bound.bottom >= top) return false;
return true; // intersection
//** Object **//
Object::Object(const AABB &bound, void *data):
_bound(bound),
_data(data)
;
void Object::setData(void *data)
_data = data;
void *Object::getData() const
return _data;
//** QuadTree **//
QuadTree::QuadTree(const AABB &bound, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level, QuadTree *parent):
_capacity(capacity),
_maxLevel(maxLevel),
_level(level),
_parent(parent),
_centerX((bound.left + bound.right) * 0.5f),
_centerY((bound.bottom + bound.top) * 0.5f),
_bound(bound)
// Inserts an object into this quadtree
bool QuadTree::insert(Object *obj)
// Item already exists
if (obj->_qt != nullptr)
return false;
// insert object at the lowest level
if (!_isLeaf)
if (QuadTree *child = getChild(obj->_bound))
return child->insert(obj);
_objects.push_back(obj);
obj->_qt = this; // used for quick search by quadtree
// Subdivide if required
if (_isLeaf && _level < _maxLevel && _objects.size() >= _capacity)
subdivide();
// Re-insert objects into new quadrant
for (unsigned i = 0; i < _objects.size();)
Object *object = _objects[i];
if (QuadTree *child = getChild(object->_bound))
_objects.erase(_objects.begin() + i);
object->_qt = nullptr;
child->insert(object);
else ++i;
return true;
// Removes an object from this quadtree
bool QuadTree::remove(Object *obj)
if (obj->_qt == nullptr)
return false;
if (obj->_qt != this)
return obj->_qt->remove(obj);
_objects.erase(std::find(_objects.begin(), _objects.end(), obj));
obj->_qt = nullptr;
discardEmptyBuckets(this);
return true;
// Removes and re-inserts object into quadtree (for objects that move)
void QuadTree::update(Object *obj)
// Checks if object exists in this quadtree
bool QuadTree::contains(Object *obj) const
if (obj->_qt == nullptr)
return false;
if (obj->_qt != this)
return obj->_qt->contains(obj);
return std::find(_objects.begin(), _objects.end(), obj) != _objects.end();
// Searches quadtree for objects within the provided boundary and returns them in callback
void QuadTree::search(const AABB &bound, const std::function<void(Object*)> &callback) const
// Search children first
if (!_isLeaf)
if (QuadTree *child = getChild(bound))
child->search(bound, callback);
else
for (auto&& node : _children)
if (node->_bound.intersects(bound))
node->search(bound, callback);
// Now search objects
for (auto&& obj : _objects)
if (obj->_bound.intersects(bound))
callback(obj);
// Searches quadtree for objects within the provided boundary and adds them to provided vector
void QuadTree::query(const AABB &bound, std::vector<Object*> &returnObjects) const
search(bound, [&returnObjects](Object *obj)
returnObjects.push_back(obj);
);
// Checks if any object exists in the specified bounds (with optional filter)
bool QuadTree::any(const AABB &bound, const std::function<bool(Object*)> &condition) const
bool found = false;
search(bound, [&condition, &found](Object *obj) );
return found;
// Returns total children count for this quadtree
unsigned QuadTree::getTotalChildren() const
unsigned count = 0;
if (!_isLeaf)
for (auto&& child : _children)
count += child->getTotalChildren();
return (_isLeaf? 0 : 4) + count;
// Returns total object count for this quadtree
unsigned QuadTree::getTotalObjects() const
unsigned count = 0;
if (!_isLeaf)
for (auto&& node : _children)
count += node->getTotalObjects();
return _objects.size() + count;
// Removes all objects and children from this quadtree
void QuadTree::clear()
if (!_objects.empty())
for (auto&& obj : _objects)
remove(obj);
_objects.clear();
if (!_isLeaf)
delete _children[0];
delete _children[1];
delete _children[2];
delete _children[3];
_isLeaf = true;
// Subdivides into four quadrants
void QuadTree::subdivide()
// Bottom right
_children[0] = new QuadTree(
_centerX, _centerY, _bound.right, _bound.bottom ,
_capacity, _maxLevel, _level + 1, this
);
// Bottom left
_children[1] = new QuadTree(
_bound.left, _centerY, _centerX, _bound.bottom ,
_capacity, _maxLevel, _level + 1, this
);
// Top left
_children[2] = new QuadTree(
_bound.left, _bound.top, _centerX, _centerY ,
_capacity, _maxLevel, _level + 1, this
);
// Top right
_children[3] = new QuadTree(
_centerX, _bound.top, _bound.right, _centerY ,
_capacity, _maxLevel, _level + 1, this
);
_isLeaf = false;
// Discards buckets if all children are leaves and contain no objects
void QuadTree::discardEmptyBuckets(QuadTree *node)
if (!node->_objects.empty())
return;
if (!node->_isLeaf)
for (auto &&child : node->_children)
if (!child->_isLeaf
node->clear();
if (node->_parent != nullptr)
discardEmptyBuckets(node->_parent);
// Returns child/quadrant that the provided boundary is in
QuadTree *QuadTree::getChild(const AABB &bound) const
bool bottom = bound.bottom > _centerY;
bool left = bound.left < _centerX;
bool top = !bottom && bound.top < _centerY;
if (left && bound.right < _centerX)
if (top) return _children[1]; // top left
if (bottom) return _children[2]; // bottom left
else if (!left)
if (top) return _children[0]; // top right
if (bottom) return _children[3]; // bottom right
return nullptr;
QuadTree::~QuadTree()
clear();
I've done some profiling in VS2017 and I have found that the insert
method takes the longest (i'm guessing due to the recursion), followed by AABB::intersects
, then search
. I have no clue on what I can do to make those methods faster. I'm curious to see what you guys have to say about it :)
c++ performance c++11 recursion tree
I've been working on a recursive quadtree in C++. How can I improve my code? My primary goals are improving the performance/speed, as well as practice.
QuadTree.h
#pragma once
#include <vector>
#include <algorithm>
#include <functional>
struct AABB
double left, top, right, bottom;
AABB()
AABB(const double &l, const double &t, const double &r, const double &b);
bool within(const AABB&) const;
bool intersects(const AABB&) const;
;
struct Object
friend class QuadTree;
public:
AABB _bound;
Object() ;
Object(const AABB&, void *data = nullptr);
void setData(void *data);
void *getData() const;
private:
void *_data = nullptr;
QuadTree *_qt = nullptr;
;
class QuadTree
public:
QuadTree(const AABB&, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level = 0, QuadTree *parent = nullptr);
bool insert(Object*);
bool remove(Object*);
void update(Object*);
bool contains(Object*) const;
void search(const AABB&, const std::function<void(Object*)> &callback) const;
void query(const AABB&, std::vector<Object*> &returnObjects) const;
bool any(const AABB&, const std::function<bool(Object*)> &condition = nullptr) const;
unsigned getTotalChildren() const;
unsigned getTotalObjects() const;
void clear();
QuadTree()
~QuadTree();
private:
AABB _bound;
QuadTree* _parent;
QuadTree* _children[4];
bool _isLeaf = true;
double _centerX;
double _centerY;
unsigned _level;
unsigned _maxLevel;
unsigned _capacity;
std::vector<Object*> _objects;
void subdivide();
void discardEmptyBuckets(QuadTree *node);
QuadTree *getChild(const AABB&) const;
;
QuadTree.cpp
#include "QuadTree.h"
//** AABB **//
AABB::AABB(const double &l, const double &t, const double &r, const double &b) :
left(l),
top(t),
right(r),
bottom(b)
bool AABB::within(const AABB &bound) const
return left > bound.left && bottom > bound.bottom
&& right < bound.right && top < bound.top;
bool AABB::intersects(const AABB &bound) const
if (bound.right <= left) return false;
if (bound.left >= right) return false;
if (bound.top <= bottom) return false;
if (bound.bottom >= top) return false;
return true; // intersection
//** Object **//
Object::Object(const AABB &bound, void *data):
_bound(bound),
_data(data)
;
void Object::setData(void *data)
_data = data;
void *Object::getData() const
return _data;
//** QuadTree **//
QuadTree::QuadTree(const AABB &bound, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level, QuadTree *parent):
_capacity(capacity),
_maxLevel(maxLevel),
_level(level),
_parent(parent),
_centerX((bound.left + bound.right) * 0.5f),
_centerY((bound.bottom + bound.top) * 0.5f),
_bound(bound)
// Inserts an object into this quadtree
bool QuadTree::insert(Object *obj)
// Item already exists
if (obj->_qt != nullptr)
return false;
// insert object at the lowest level
if (!_isLeaf)
if (QuadTree *child = getChild(obj->_bound))
return child->insert(obj);
_objects.push_back(obj);
obj->_qt = this; // used for quick search by quadtree
// Subdivide if required
if (_isLeaf && _level < _maxLevel && _objects.size() >= _capacity)
subdivide();
// Re-insert objects into new quadrant
for (unsigned i = 0; i < _objects.size();)
Object *object = _objects[i];
if (QuadTree *child = getChild(object->_bound))
_objects.erase(_objects.begin() + i);
object->_qt = nullptr;
child->insert(object);
else ++i;
return true;
// Removes an object from this quadtree
bool QuadTree::remove(Object *obj)
if (obj->_qt == nullptr)
return false;
if (obj->_qt != this)
return obj->_qt->remove(obj);
_objects.erase(std::find(_objects.begin(), _objects.end(), obj));
obj->_qt = nullptr;
discardEmptyBuckets(this);
return true;
// Removes and re-inserts object into quadtree (for objects that move)
void QuadTree::update(Object *obj)
// Checks if object exists in this quadtree
bool QuadTree::contains(Object *obj) const
if (obj->_qt == nullptr)
return false;
if (obj->_qt != this)
return obj->_qt->contains(obj);
return std::find(_objects.begin(), _objects.end(), obj) != _objects.end();
// Searches quadtree for objects within the provided boundary and returns them in callback
void QuadTree::search(const AABB &bound, const std::function<void(Object*)> &callback) const
// Search children first
if (!_isLeaf)
if (QuadTree *child = getChild(bound))
child->search(bound, callback);
else
for (auto&& node : _children)
if (node->_bound.intersects(bound))
node->search(bound, callback);
// Now search objects
for (auto&& obj : _objects)
if (obj->_bound.intersects(bound))
callback(obj);
// Searches quadtree for objects within the provided boundary and adds them to provided vector
void QuadTree::query(const AABB &bound, std::vector<Object*> &returnObjects) const
search(bound, [&returnObjects](Object *obj)
returnObjects.push_back(obj);
);
// Checks if any object exists in the specified bounds (with optional filter)
bool QuadTree::any(const AABB &bound, const std::function<bool(Object*)> &condition) const
bool found = false;
search(bound, [&condition, &found](Object *obj) );
return found;
// Returns total children count for this quadtree
unsigned QuadTree::getTotalChildren() const
unsigned count = 0;
if (!_isLeaf)
for (auto&& child : _children)
count += child->getTotalChildren();
return (_isLeaf? 0 : 4) + count;
// Returns total object count for this quadtree
unsigned QuadTree::getTotalObjects() const
unsigned count = 0;
if (!_isLeaf)
for (auto&& node : _children)
count += node->getTotalObjects();
return _objects.size() + count;
// Removes all objects and children from this quadtree
void QuadTree::clear()
if (!_objects.empty())
for (auto&& obj : _objects)
remove(obj);
_objects.clear();
if (!_isLeaf)
delete _children[0];
delete _children[1];
delete _children[2];
delete _children[3];
_isLeaf = true;
// Subdivides into four quadrants
void QuadTree::subdivide()
// Bottom right
_children[0] = new QuadTree(
_centerX, _centerY, _bound.right, _bound.bottom ,
_capacity, _maxLevel, _level + 1, this
);
// Bottom left
_children[1] = new QuadTree(
_bound.left, _centerY, _centerX, _bound.bottom ,
_capacity, _maxLevel, _level + 1, this
);
// Top left
_children[2] = new QuadTree(
_bound.left, _bound.top, _centerX, _centerY ,
_capacity, _maxLevel, _level + 1, this
);
// Top right
_children[3] = new QuadTree(
_centerX, _bound.top, _bound.right, _centerY ,
_capacity, _maxLevel, _level + 1, this
);
_isLeaf = false;
// Discards buckets if all children are leaves and contain no objects
void QuadTree::discardEmptyBuckets(QuadTree *node)
if (!node->_objects.empty())
return;
if (!node->_isLeaf)
for (auto &&child : node->_children)
if (!child->_isLeaf
node->clear();
if (node->_parent != nullptr)
discardEmptyBuckets(node->_parent);
// Returns child/quadrant that the provided boundary is in
QuadTree *QuadTree::getChild(const AABB &bound) const
bool bottom = bound.bottom > _centerY;
bool left = bound.left < _centerX;
bool top = !bottom && bound.top < _centerY;
if (left && bound.right < _centerX)
if (top) return _children[1]; // top left
if (bottom) return _children[2]; // bottom left
else if (!left)
if (top) return _children[0]; // top right
if (bottom) return _children[3]; // bottom right
return nullptr;
QuadTree::~QuadTree()
clear();
I've done some profiling in VS2017 and I have found that the insert
method takes the longest (i'm guessing due to the recursion), followed by AABB::intersects
, then search
. I have no clue on what I can do to make those methods faster. I'm curious to see what you guys have to say about it :)
c++ performance c++11 recursion tree
edited May 21 at 3:30
bruglesco
9321518
9321518
asked May 21 at 2:04
m-byte
312
312
I'm a little confused reading this. If my root-levelQuadTree
has bounds from, say, -1 to +1 in both directions, it will be subdivided into 4 quads from 0 to ±1, etc. If I have an object that's centered at the origin, it looks to me like it will only end up in one of the second-level quads, not all 4, which is where it needs to be. Am I misunderstanding that?
â user1118321
May 21 at 3:00
It depends on the size of the object. It will only be re-inserted into one of the second-level quadrants if its boundaries can fit inside of one (aka ifgetChild
does not return nullptr), otherwise, it is skipped and remains inside all four quadrants. I should probably make that more clear :P
â m-byte
May 21 at 3:51
remember to upvote helpful answers!
â JDà Âugosz
May 23 at 0:46
What are you using this quadtree for ? collision detection, finding closest pairs ??
â Harald Scheirich
May 23 at 1:01
@HaraldScheirich I'm using it mainly for collision detection.
â m-byte
May 23 at 1:40
add a comment |Â
I'm a little confused reading this. If my root-levelQuadTree
has bounds from, say, -1 to +1 in both directions, it will be subdivided into 4 quads from 0 to ±1, etc. If I have an object that's centered at the origin, it looks to me like it will only end up in one of the second-level quads, not all 4, which is where it needs to be. Am I misunderstanding that?
â user1118321
May 21 at 3:00
It depends on the size of the object. It will only be re-inserted into one of the second-level quadrants if its boundaries can fit inside of one (aka ifgetChild
does not return nullptr), otherwise, it is skipped and remains inside all four quadrants. I should probably make that more clear :P
â m-byte
May 21 at 3:51
remember to upvote helpful answers!
â JDà Âugosz
May 23 at 0:46
What are you using this quadtree for ? collision detection, finding closest pairs ??
â Harald Scheirich
May 23 at 1:01
@HaraldScheirich I'm using it mainly for collision detection.
â m-byte
May 23 at 1:40
I'm a little confused reading this. If my root-level
QuadTree
has bounds from, say, -1 to +1 in both directions, it will be subdivided into 4 quads from 0 to ±1, etc. If I have an object that's centered at the origin, it looks to me like it will only end up in one of the second-level quads, not all 4, which is where it needs to be. Am I misunderstanding that?â user1118321
May 21 at 3:00
I'm a little confused reading this. If my root-level
QuadTree
has bounds from, say, -1 to +1 in both directions, it will be subdivided into 4 quads from 0 to ±1, etc. If I have an object that's centered at the origin, it looks to me like it will only end up in one of the second-level quads, not all 4, which is where it needs to be. Am I misunderstanding that?â user1118321
May 21 at 3:00
It depends on the size of the object. It will only be re-inserted into one of the second-level quadrants if its boundaries can fit inside of one (aka if
getChild
does not return nullptr), otherwise, it is skipped and remains inside all four quadrants. I should probably make that more clear :Pâ m-byte
May 21 at 3:51
It depends on the size of the object. It will only be re-inserted into one of the second-level quadrants if its boundaries can fit inside of one (aka if
getChild
does not return nullptr), otherwise, it is skipped and remains inside all four quadrants. I should probably make that more clear :Pâ m-byte
May 21 at 3:51
remember to upvote helpful answers!
â JDà Âugosz
May 23 at 0:46
remember to upvote helpful answers!
â JDà Âugosz
May 23 at 0:46
What are you using this quadtree for ? collision detection, finding closest pairs ??
â Harald Scheirich
May 23 at 1:01
What are you using this quadtree for ? collision detection, finding closest pairs ??
â Harald Scheirich
May 23 at 1:01
@HaraldScheirich I'm using it mainly for collision detection.
â m-byte
May 23 at 1:40
@HaraldScheirich I'm using it mainly for collision detection.
â m-byte
May 23 at 1:40
add a comment |Â
3 Answers
3
active
oldest
votes
up vote
3
down vote
AABB()
Object() ;
QuadTree()
What does this do, compared with not mentioning it at all? I notice you add an empty default constructor to all your classes. Look up âÂÂthe rule of fiveâÂÂ. In the case of AABB
, you can remove the other constructor and then aggregate initialization takes hold, which is the same thing you wanted. Getting rid of it means I donâÂÂt have to wonder why you are passing double
by const ref instead of value.
And you have a stray ;
.
Writing identifiers with a leading underscore is a bad idea, and is generally dissuaded as a style. Note that it is legal for member names if (and only if) the next character is not a capital letter or another underscore.
The Object
default constructor simply allows the default data initializers to apply, so it is not necessary (and can be worse) to write it explicitly. You can use =default
if you need to mention it, but leaving it off completely is best.
OTOH, you did not define a copy constructor etc. What happens when an instance is copied? The pointer members get shallow copied, which generally is not right.
Raw pointers? what is the ownership/relationship/lifetime of these data members?
A void* data
member? DonâÂÂt do that. There are safe ways to do type erasure in C++, or templates to customize the payload type, std::any
, etc.
QuadTree(const AABB&, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level = 0, QuadTree *parent = nullptr);
Why would you pass an unsigned
as a const ref?
void search(const AABB&, const std::function<void(Object*)> &callback) const;
How do you use this? Normally a search
takes a predicate but you have it labeled callback
, and the first parameter is unnamed. I think it would be clearer to call it enumerate
(If I understand what it does) and name the searched-for rectangle to be clear thatâÂÂs what it is for.
void query(const AABB&, std::vector<Object*> &returnObjects) const;
DonâÂÂt use âÂÂoutâ parameters. Return values (⧺F.20).
std::vector<Object*> query (const AABB&) const;
You might get faster code if you declared within
, intersects
and whatever else you can as noexcept
.
ItâÂÂs odd that within
and intersects
are written in totally different styles when they work in the identical manner.
speed of insert
Look at branch prediction. If an if
statement will go one way or the other at random (as when traversing a tree) the predictor will never get it right and you have a huge latency.
_children[0] = new QuadTree(
⧺C.149 â no naked new
or delete
.
I also think that allocating nodes as part of insert
is why it is slow! That is inherently a slow operation. You might time the subdivide
function separately and compare that to the insert
time, and see what percentage it accounts for.
Thank you for your feedback :) In the case ofObject
, I decided to just disable the copy constructor all together, sinceQuadTree
stores a vector of pointers to Objects, copying them to other objects would render functions likeremove()
andcontains()
useless. Also that makes sense whyinsert
is slow, I'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.
â m-byte
May 22 at 20:32
Also, would return values really work better than out parameters in this case? I read somewhere that returning vectors from functions is not a good idea, because the vector has a chance of being copied on the way back. For example, if I have a quadtree with 100,000 objects, and the query returns about 10,000 objects pointers, something likestd::vector<Object*> foundObjects = quadTree.query(bound);
could copy all 10,000 pointers tofoundObjects
. Or is this not the case with modern compilers?
â m-byte
May 22 at 20:41
The vector will not be copied! Even if you donâÂÂt do things just right to allow NRVO, since C++11 we have move semantics. Returning the vector will just copy the pointer to the guts.
â JDà Âugosz
May 23 at 0:30
âªI'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.â« I donâÂÂt know what you mean by that.
â JDà Âugosz
May 23 at 0:32
See this video from four years ago. Returning large values starts at 14:30. A minute farther, âÂÂreturning a potentially large matrix by value ⦠and itâÂÂs OK, because we automatically move from temporary objects; we donâÂÂt do deep copies â so you get the clean semantics of value types and the efficiency of reference typesâÂÂ
â JDà Âugosz
May 23 at 0:44
 |Â
show 3 more comments
up vote
3
down vote
Quadtrees are very useful, so this is a good exercise to implement your own. I think you've done a reasonable job here, but I see some things that could be improved.
Naming
Always name all of your parameters in your header. It makes it so much easier for another programmer that's trying to call the methods in a class if the parameters have useful, descriptive names. For example, in the Object
constructor, what is the AABB
parameter? Is it the bounds of the object? Is it the bounds of the quad in a QuadTree
in which the object is found? Please clarify!
Speaking of the Object
class, the name Object
is meaningless. Every single thing in your program (other than PODT) are objects, yet they can't all be inserted into a QuadTree
. Usually quad trees are used for things like collision-detection or finding the distance to neighbors. I would rename the Object
class to Collidable
or something along those lines.
What is the difference between contains()
, search()
and query()
? Just looking at the names, they all sound like the same thing. You should give them more distinct names that clarify what they actually do. Looking at the code, it seems that contains()
does a shallow search of the object's QuadTree
for the existence of the object. (Isn't that a tautology? Shouldn't you never be allowed to have an object's _qt
be a QuadTree
to which it does not belong?) search()
calls a callback for every object in a bounding box. So maybe a better name would be performTaskOnObjectsInRegion()
or something like that. And query()
appears to collect all objects within a bounding box, so I'd call it collectObjectsInRegion()
or something along those lines.
Default Constructors
All 3 of your classes have default constructors that do nothing and leave their member variables in an undefined state. Why? It would be better to disallow creating these objects in an unusable state. Instead of an axis-aligned bounding box that contains random (possibly nonsensical) values for its bounds, simply disallow creating one without bounds by using:
AABB() = delete;
This removes the default constructor and solves the problem of an invalid object being created. Likewise, if you do the same for Object
, then you never need for its _bound
to be invalid. You can then also remove the setData()
method as its data will always be set in the constructor. (I'd also remove the default nullptr
value as there's no use in having an Object
with no data
.) Likewise with QuadTree
. Doing this eliminates a huge class of bugs that crop up from badly initialized instances of objects.
Performance
I ran a profile of inserting 100,000 objects into a QuadTree
. The slowest part (84% of runtime in my experiment) is the call to vector::erase()
. The reason is that this causes the rest of the elements in the vector to be moved back 1 spot. It ends up being a bunch of calls to memmove()
. It might be worth trying other containers instead of std::vector
. One that has O(1) removal would fix this problem. Or, you could swap the object that you're currently working on with the last object in the vector
and then erase only the last object. Doing this removed insert()
from even showing up in my traces. (I didn't do much verification of the resulting QuadTree
because there are no tests here, and no examples of how to use it, so make sure you do a lot of tests before going with that method.) It looked like this:
// Re-insert objects into new quadrant
for (unsigned i = 0; i < _objects.size();)
Object *object = _objects[i];
if (QuadTree *child = getChild(object->_bound))
std::swap(*(_objects.begin() + i), *(_objects.end() - 1));
_objects.erase(_objects.end() - 1);
object->_qt = nullptr;
child->insert(object);
else ++i;
stackoverflow.com/q/347441/5416291 there's a term for that.
â bruglesco
May 22 at 13:52
Thank you! I'm in the process of renaming most of the program now. I thought thatcontains()
would be a useful since an object'sqt
isn't meant to be accessed by anything other than theQuadTree
itself, so anyone who would like to check can just use that instead of the if statement. Also the reason why I had default constructors for every class is so I could initialize them later before I insert them into the quadtree.. but I see now that it was a bad idea haha. I'll make sure that isn't possible.
â m-byte
May 22 at 20:06
Also whyinsert
was slow makes a lot of sense now. I didn't realize how muchvector::erase()
was bottle necking my code until I actually thought about what it was doing. I'll try your method and see if it holds up, if not Ill try your suggestion of using another container. Which one would you suggest would work best in this scenario?
â m-byte
May 22 at 20:18
My first thought was to use astd::set
instead ofstd::vector
, since you don't care about the order of the elements. But there's a tradeoff in that insertions are O(log(n)). If the above swap and delete works, I'd just keep that.
â user1118321
May 23 at 3:22
add a comment |Â
up vote
1
down vote
Some things I haven't seen in the previous reviews, or I just plain missed ...
Quadtree
With moving object the quadtree might not be your best choice, at best an object will move from one child into the next but at worst you could have to delete a whole bunch as the path that the item is on might become empty and you have to remove all the children. Having multiple objects per node also forces you to keep a dynamic data structure in the node, that is not ideal. An alternative solution is the AABB Tree, (http://www.randygaul.net/2013/08/06/dynamic-aabb-tree/) has a good description. As this tree only has one object per leaf at max and has a max number of nodes you can preallocate the memory and you can use a linear store for all the nodes. Additionally it is possible to change the object AABBs and update the structure with new bounding boxes. If updating the tree usually you'll check for an amount of change that will trigger a rebuild.
Depending on the number of objects, you might just be quicker doing a pairwise AABB test, did you do the timing ?
Allocations
Each insertion and removal incurs a good number of deallocations and allocations, if you want to stick with the quad-tree you can save on allocations by preserving children that you have already allocated and only allocating when needed.
update()
Your update calls remove()
and insert()
which updates the array twice, that is not optimal. If you're doing collision detection, in most cases you'll probably have a bunch of updates to do at the same time. If you can utilize this you might be able to improve your performance.
Do you really need it ?
_maxLevel
and_level
are never read in the code, do you really need it ?contains()
are you sure you ever need to test if a specific object is in the tree ? Meaning are you ever in a state where there are objects that might be in the tree ? If you can get rid of that function, you can get rid of the trackingstd::vector<>
the maintainance of which cost a lot of time.
_children
_children
could be a std::array
rather than a c-array;
Formatting
I'm not a fan of use of braces like this
if (!_isLeaf)
if (QuadTree *child = getChild(obj->_bound))
return child->insert(obj);
What made you put the first pair of brace in ? Just because it looks complicated ? A rule of thumb I use is, "you can skip the braces if it fits in one line, e.g.
if (obj->_qt != nullptr) return false;
Naming
contains
, query
, search
, any
do very similar things, contains
, getObjectsInBound
, search
could be labeled as forEachObjectInBound
(similar to std::for_each
with bounds), and any is findEachObjectInBoundIf
(similar to std::find_if
with bounds)
Consider std::algorithm
Your trio of query
, search
and any
could be replaced by always using the following search()
void QuadTree::query(const AABB &bound, std::vector<Object*> &returnObjects) const
if (!_isLeaf)
if (QuadTree *child = getChild(bound))
child->search(bound, returnObjects);
else
for (auto&& node : _children)
if (node->_bound.intersects(bound))
node->search(bound, returnObjects);
// Now search objects
for (auto&& obj : _objects)
if (obj->_bound.intersects(bound))
returnObjects.push_back(obj);
And then use std::foreach
or std::find_if
on it. Although if you use any()
a lot you would want to implement it in a way it exits as sound as the condition is satisfied, your implementation finds all objects no matter if the condition is true early.
add a comment |Â
3 Answers
3
active
oldest
votes
3 Answers
3
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
3
down vote
AABB()
Object() ;
QuadTree()
What does this do, compared with not mentioning it at all? I notice you add an empty default constructor to all your classes. Look up âÂÂthe rule of fiveâÂÂ. In the case of AABB
, you can remove the other constructor and then aggregate initialization takes hold, which is the same thing you wanted. Getting rid of it means I donâÂÂt have to wonder why you are passing double
by const ref instead of value.
And you have a stray ;
.
Writing identifiers with a leading underscore is a bad idea, and is generally dissuaded as a style. Note that it is legal for member names if (and only if) the next character is not a capital letter or another underscore.
The Object
default constructor simply allows the default data initializers to apply, so it is not necessary (and can be worse) to write it explicitly. You can use =default
if you need to mention it, but leaving it off completely is best.
OTOH, you did not define a copy constructor etc. What happens when an instance is copied? The pointer members get shallow copied, which generally is not right.
Raw pointers? what is the ownership/relationship/lifetime of these data members?
A void* data
member? DonâÂÂt do that. There are safe ways to do type erasure in C++, or templates to customize the payload type, std::any
, etc.
QuadTree(const AABB&, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level = 0, QuadTree *parent = nullptr);
Why would you pass an unsigned
as a const ref?
void search(const AABB&, const std::function<void(Object*)> &callback) const;
How do you use this? Normally a search
takes a predicate but you have it labeled callback
, and the first parameter is unnamed. I think it would be clearer to call it enumerate
(If I understand what it does) and name the searched-for rectangle to be clear thatâÂÂs what it is for.
void query(const AABB&, std::vector<Object*> &returnObjects) const;
DonâÂÂt use âÂÂoutâ parameters. Return values (⧺F.20).
std::vector<Object*> query (const AABB&) const;
You might get faster code if you declared within
, intersects
and whatever else you can as noexcept
.
ItâÂÂs odd that within
and intersects
are written in totally different styles when they work in the identical manner.
speed of insert
Look at branch prediction. If an if
statement will go one way or the other at random (as when traversing a tree) the predictor will never get it right and you have a huge latency.
_children[0] = new QuadTree(
⧺C.149 â no naked new
or delete
.
I also think that allocating nodes as part of insert
is why it is slow! That is inherently a slow operation. You might time the subdivide
function separately and compare that to the insert
time, and see what percentage it accounts for.
Thank you for your feedback :) In the case ofObject
, I decided to just disable the copy constructor all together, sinceQuadTree
stores a vector of pointers to Objects, copying them to other objects would render functions likeremove()
andcontains()
useless. Also that makes sense whyinsert
is slow, I'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.
â m-byte
May 22 at 20:32
Also, would return values really work better than out parameters in this case? I read somewhere that returning vectors from functions is not a good idea, because the vector has a chance of being copied on the way back. For example, if I have a quadtree with 100,000 objects, and the query returns about 10,000 objects pointers, something likestd::vector<Object*> foundObjects = quadTree.query(bound);
could copy all 10,000 pointers tofoundObjects
. Or is this not the case with modern compilers?
â m-byte
May 22 at 20:41
The vector will not be copied! Even if you donâÂÂt do things just right to allow NRVO, since C++11 we have move semantics. Returning the vector will just copy the pointer to the guts.
â JDà Âugosz
May 23 at 0:30
âªI'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.â« I donâÂÂt know what you mean by that.
â JDà Âugosz
May 23 at 0:32
See this video from four years ago. Returning large values starts at 14:30. A minute farther, âÂÂreturning a potentially large matrix by value ⦠and itâÂÂs OK, because we automatically move from temporary objects; we donâÂÂt do deep copies â so you get the clean semantics of value types and the efficiency of reference typesâÂÂ
â JDà Âugosz
May 23 at 0:44
 |Â
show 3 more comments
up vote
3
down vote
AABB()
Object() ;
QuadTree()
What does this do, compared with not mentioning it at all? I notice you add an empty default constructor to all your classes. Look up âÂÂthe rule of fiveâÂÂ. In the case of AABB
, you can remove the other constructor and then aggregate initialization takes hold, which is the same thing you wanted. Getting rid of it means I donâÂÂt have to wonder why you are passing double
by const ref instead of value.
And you have a stray ;
.
Writing identifiers with a leading underscore is a bad idea, and is generally dissuaded as a style. Note that it is legal for member names if (and only if) the next character is not a capital letter or another underscore.
The Object
default constructor simply allows the default data initializers to apply, so it is not necessary (and can be worse) to write it explicitly. You can use =default
if you need to mention it, but leaving it off completely is best.
OTOH, you did not define a copy constructor etc. What happens when an instance is copied? The pointer members get shallow copied, which generally is not right.
Raw pointers? what is the ownership/relationship/lifetime of these data members?
A void* data
member? DonâÂÂt do that. There are safe ways to do type erasure in C++, or templates to customize the payload type, std::any
, etc.
QuadTree(const AABB&, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level = 0, QuadTree *parent = nullptr);
Why would you pass an unsigned
as a const ref?
void search(const AABB&, const std::function<void(Object*)> &callback) const;
How do you use this? Normally a search
takes a predicate but you have it labeled callback
, and the first parameter is unnamed. I think it would be clearer to call it enumerate
(If I understand what it does) and name the searched-for rectangle to be clear thatâÂÂs what it is for.
void query(const AABB&, std::vector<Object*> &returnObjects) const;
DonâÂÂt use âÂÂoutâ parameters. Return values (⧺F.20).
std::vector<Object*> query (const AABB&) const;
You might get faster code if you declared within
, intersects
and whatever else you can as noexcept
.
ItâÂÂs odd that within
and intersects
are written in totally different styles when they work in the identical manner.
speed of insert
Look at branch prediction. If an if
statement will go one way or the other at random (as when traversing a tree) the predictor will never get it right and you have a huge latency.
_children[0] = new QuadTree(
⧺C.149 â no naked new
or delete
.
I also think that allocating nodes as part of insert
is why it is slow! That is inherently a slow operation. You might time the subdivide
function separately and compare that to the insert
time, and see what percentage it accounts for.
Thank you for your feedback :) In the case ofObject
, I decided to just disable the copy constructor all together, sinceQuadTree
stores a vector of pointers to Objects, copying them to other objects would render functions likeremove()
andcontains()
useless. Also that makes sense whyinsert
is slow, I'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.
â m-byte
May 22 at 20:32
Also, would return values really work better than out parameters in this case? I read somewhere that returning vectors from functions is not a good idea, because the vector has a chance of being copied on the way back. For example, if I have a quadtree with 100,000 objects, and the query returns about 10,000 objects pointers, something likestd::vector<Object*> foundObjects = quadTree.query(bound);
could copy all 10,000 pointers tofoundObjects
. Or is this not the case with modern compilers?
â m-byte
May 22 at 20:41
The vector will not be copied! Even if you donâÂÂt do things just right to allow NRVO, since C++11 we have move semantics. Returning the vector will just copy the pointer to the guts.
â JDà Âugosz
May 23 at 0:30
âªI'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.â« I donâÂÂt know what you mean by that.
â JDà Âugosz
May 23 at 0:32
See this video from four years ago. Returning large values starts at 14:30. A minute farther, âÂÂreturning a potentially large matrix by value ⦠and itâÂÂs OK, because we automatically move from temporary objects; we donâÂÂt do deep copies â so you get the clean semantics of value types and the efficiency of reference typesâÂÂ
â JDà Âugosz
May 23 at 0:44
 |Â
show 3 more comments
up vote
3
down vote
up vote
3
down vote
AABB()
Object() ;
QuadTree()
What does this do, compared with not mentioning it at all? I notice you add an empty default constructor to all your classes. Look up âÂÂthe rule of fiveâÂÂ. In the case of AABB
, you can remove the other constructor and then aggregate initialization takes hold, which is the same thing you wanted. Getting rid of it means I donâÂÂt have to wonder why you are passing double
by const ref instead of value.
And you have a stray ;
.
Writing identifiers with a leading underscore is a bad idea, and is generally dissuaded as a style. Note that it is legal for member names if (and only if) the next character is not a capital letter or another underscore.
The Object
default constructor simply allows the default data initializers to apply, so it is not necessary (and can be worse) to write it explicitly. You can use =default
if you need to mention it, but leaving it off completely is best.
OTOH, you did not define a copy constructor etc. What happens when an instance is copied? The pointer members get shallow copied, which generally is not right.
Raw pointers? what is the ownership/relationship/lifetime of these data members?
A void* data
member? DonâÂÂt do that. There are safe ways to do type erasure in C++, or templates to customize the payload type, std::any
, etc.
QuadTree(const AABB&, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level = 0, QuadTree *parent = nullptr);
Why would you pass an unsigned
as a const ref?
void search(const AABB&, const std::function<void(Object*)> &callback) const;
How do you use this? Normally a search
takes a predicate but you have it labeled callback
, and the first parameter is unnamed. I think it would be clearer to call it enumerate
(If I understand what it does) and name the searched-for rectangle to be clear thatâÂÂs what it is for.
void query(const AABB&, std::vector<Object*> &returnObjects) const;
DonâÂÂt use âÂÂoutâ parameters. Return values (⧺F.20).
std::vector<Object*> query (const AABB&) const;
You might get faster code if you declared within
, intersects
and whatever else you can as noexcept
.
ItâÂÂs odd that within
and intersects
are written in totally different styles when they work in the identical manner.
speed of insert
Look at branch prediction. If an if
statement will go one way or the other at random (as when traversing a tree) the predictor will never get it right and you have a huge latency.
_children[0] = new QuadTree(
⧺C.149 â no naked new
or delete
.
I also think that allocating nodes as part of insert
is why it is slow! That is inherently a slow operation. You might time the subdivide
function separately and compare that to the insert
time, and see what percentage it accounts for.
AABB()
Object() ;
QuadTree()
What does this do, compared with not mentioning it at all? I notice you add an empty default constructor to all your classes. Look up âÂÂthe rule of fiveâÂÂ. In the case of AABB
, you can remove the other constructor and then aggregate initialization takes hold, which is the same thing you wanted. Getting rid of it means I donâÂÂt have to wonder why you are passing double
by const ref instead of value.
And you have a stray ;
.
Writing identifiers with a leading underscore is a bad idea, and is generally dissuaded as a style. Note that it is legal for member names if (and only if) the next character is not a capital letter or another underscore.
The Object
default constructor simply allows the default data initializers to apply, so it is not necessary (and can be worse) to write it explicitly. You can use =default
if you need to mention it, but leaving it off completely is best.
OTOH, you did not define a copy constructor etc. What happens when an instance is copied? The pointer members get shallow copied, which generally is not right.
Raw pointers? what is the ownership/relationship/lifetime of these data members?
A void* data
member? DonâÂÂt do that. There are safe ways to do type erasure in C++, or templates to customize the payload type, std::any
, etc.
QuadTree(const AABB&, const unsigned &capacity, const unsigned &maxLevel,
const unsigned &level = 0, QuadTree *parent = nullptr);
Why would you pass an unsigned
as a const ref?
void search(const AABB&, const std::function<void(Object*)> &callback) const;
How do you use this? Normally a search
takes a predicate but you have it labeled callback
, and the first parameter is unnamed. I think it would be clearer to call it enumerate
(If I understand what it does) and name the searched-for rectangle to be clear thatâÂÂs what it is for.
void query(const AABB&, std::vector<Object*> &returnObjects) const;
DonâÂÂt use âÂÂoutâ parameters. Return values (⧺F.20).
std::vector<Object*> query (const AABB&) const;
You might get faster code if you declared within
, intersects
and whatever else you can as noexcept
.
ItâÂÂs odd that within
and intersects
are written in totally different styles when they work in the identical manner.
speed of insert
Look at branch prediction. If an if
statement will go one way or the other at random (as when traversing a tree) the predictor will never get it right and you have a huge latency.
_children[0] = new QuadTree(
⧺C.149 â no naked new
or delete
.
I also think that allocating nodes as part of insert
is why it is slow! That is inherently a slow operation. You might time the subdivide
function separately and compare that to the insert
time, and see what percentage it accounts for.
edited May 21 at 5:02
answered May 21 at 4:52
JDÃ Âugosz
5,047731
5,047731
Thank you for your feedback :) In the case ofObject
, I decided to just disable the copy constructor all together, sinceQuadTree
stores a vector of pointers to Objects, copying them to other objects would render functions likeremove()
andcontains()
useless. Also that makes sense whyinsert
is slow, I'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.
â m-byte
May 22 at 20:32
Also, would return values really work better than out parameters in this case? I read somewhere that returning vectors from functions is not a good idea, because the vector has a chance of being copied on the way back. For example, if I have a quadtree with 100,000 objects, and the query returns about 10,000 objects pointers, something likestd::vector<Object*> foundObjects = quadTree.query(bound);
could copy all 10,000 pointers tofoundObjects
. Or is this not the case with modern compilers?
â m-byte
May 22 at 20:41
The vector will not be copied! Even if you donâÂÂt do things just right to allow NRVO, since C++11 we have move semantics. Returning the vector will just copy the pointer to the guts.
â JDà Âugosz
May 23 at 0:30
âªI'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.â« I donâÂÂt know what you mean by that.
â JDà Âugosz
May 23 at 0:32
See this video from four years ago. Returning large values starts at 14:30. A minute farther, âÂÂreturning a potentially large matrix by value ⦠and itâÂÂs OK, because we automatically move from temporary objects; we donâÂÂt do deep copies â so you get the clean semantics of value types and the efficiency of reference typesâÂÂ
â JDà Âugosz
May 23 at 0:44
 |Â
show 3 more comments
Thank you for your feedback :) In the case ofObject
, I decided to just disable the copy constructor all together, sinceQuadTree
stores a vector of pointers to Objects, copying them to other objects would render functions likeremove()
andcontains()
useless. Also that makes sense whyinsert
is slow, I'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.
â m-byte
May 22 at 20:32
Also, would return values really work better than out parameters in this case? I read somewhere that returning vectors from functions is not a good idea, because the vector has a chance of being copied on the way back. For example, if I have a quadtree with 100,000 objects, and the query returns about 10,000 objects pointers, something likestd::vector<Object*> foundObjects = quadTree.query(bound);
could copy all 10,000 pointers tofoundObjects
. Or is this not the case with modern compilers?
â m-byte
May 22 at 20:41
The vector will not be copied! Even if you donâÂÂt do things just right to allow NRVO, since C++11 we have move semantics. Returning the vector will just copy the pointer to the guts.
â JDà Âugosz
May 23 at 0:30
âªI'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.â« I donâÂÂt know what you mean by that.
â JDà Âugosz
May 23 at 0:32
See this video from four years ago. Returning large values starts at 14:30. A minute farther, âÂÂreturning a potentially large matrix by value ⦠and itâÂÂs OK, because we automatically move from temporary objects; we donâÂÂt do deep copies â so you get the clean semantics of value types and the efficiency of reference typesâÂÂ
â JDà Âugosz
May 23 at 0:44
Thank you for your feedback :) In the case of
Object
, I decided to just disable the copy constructor all together, since QuadTree
stores a vector of pointers to Objects, copying them to other objects would render functions like remove()
and contains()
useless. Also that makes sense why insert
is slow, I'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.â m-byte
May 22 at 20:32
Thank you for your feedback :) In the case of
Object
, I decided to just disable the copy constructor all together, since QuadTree
stores a vector of pointers to Objects, copying them to other objects would render functions like remove()
and contains()
useless. Also that makes sense why insert
is slow, I'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.â m-byte
May 22 at 20:32
Also, would return values really work better than out parameters in this case? I read somewhere that returning vectors from functions is not a good idea, because the vector has a chance of being copied on the way back. For example, if I have a quadtree with 100,000 objects, and the query returns about 10,000 objects pointers, something like
std::vector<Object*> foundObjects = quadTree.query(bound);
could copy all 10,000 pointers to foundObjects
. Or is this not the case with modern compilers?â m-byte
May 22 at 20:41
Also, would return values really work better than out parameters in this case? I read somewhere that returning vectors from functions is not a good idea, because the vector has a chance of being copied on the way back. For example, if I have a quadtree with 100,000 objects, and the query returns about 10,000 objects pointers, something like
std::vector<Object*> foundObjects = quadTree.query(bound);
could copy all 10,000 pointers to foundObjects
. Or is this not the case with modern compilers?â m-byte
May 22 at 20:41
The vector will not be copied! Even if you donâÂÂt do things just right to allow NRVO, since C++11 we have move semantics. Returning the vector will just copy the pointer to the guts.
â JDà Âugosz
May 23 at 0:30
The vector will not be copied! Even if you donâÂÂt do things just right to allow NRVO, since C++11 we have move semantics. Returning the vector will just copy the pointer to the guts.
â JDà Âugosz
May 23 at 0:30
âªI'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.â« I donâÂÂt know what you mean by that.
â JDà Âugosz
May 23 at 0:32
âªI'll just make it so that they are not allocated on the heap and I add them to the array by reference instead.â« I donâÂÂt know what you mean by that.
â JDà Âugosz
May 23 at 0:32
See this video from four years ago. Returning large values starts at 14:30. A minute farther, âÂÂreturning a potentially large matrix by value ⦠and itâÂÂs OK, because we automatically move from temporary objects; we donâÂÂt do deep copies â so you get the clean semantics of value types and the efficiency of reference typesâÂÂ
â JDà Âugosz
May 23 at 0:44
See this video from four years ago. Returning large values starts at 14:30. A minute farther, âÂÂreturning a potentially large matrix by value ⦠and itâÂÂs OK, because we automatically move from temporary objects; we donâÂÂt do deep copies â so you get the clean semantics of value types and the efficiency of reference typesâÂÂ
â JDà Âugosz
May 23 at 0:44
 |Â
show 3 more comments
up vote
3
down vote
Quadtrees are very useful, so this is a good exercise to implement your own. I think you've done a reasonable job here, but I see some things that could be improved.
Naming
Always name all of your parameters in your header. It makes it so much easier for another programmer that's trying to call the methods in a class if the parameters have useful, descriptive names. For example, in the Object
constructor, what is the AABB
parameter? Is it the bounds of the object? Is it the bounds of the quad in a QuadTree
in which the object is found? Please clarify!
Speaking of the Object
class, the name Object
is meaningless. Every single thing in your program (other than PODT) are objects, yet they can't all be inserted into a QuadTree
. Usually quad trees are used for things like collision-detection or finding the distance to neighbors. I would rename the Object
class to Collidable
or something along those lines.
What is the difference between contains()
, search()
and query()
? Just looking at the names, they all sound like the same thing. You should give them more distinct names that clarify what they actually do. Looking at the code, it seems that contains()
does a shallow search of the object's QuadTree
for the existence of the object. (Isn't that a tautology? Shouldn't you never be allowed to have an object's _qt
be a QuadTree
to which it does not belong?) search()
calls a callback for every object in a bounding box. So maybe a better name would be performTaskOnObjectsInRegion()
or something like that. And query()
appears to collect all objects within a bounding box, so I'd call it collectObjectsInRegion()
or something along those lines.
Default Constructors
All 3 of your classes have default constructors that do nothing and leave their member variables in an undefined state. Why? It would be better to disallow creating these objects in an unusable state. Instead of an axis-aligned bounding box that contains random (possibly nonsensical) values for its bounds, simply disallow creating one without bounds by using:
AABB() = delete;
This removes the default constructor and solves the problem of an invalid object being created. Likewise, if you do the same for Object
, then you never need for its _bound
to be invalid. You can then also remove the setData()
method as its data will always be set in the constructor. (I'd also remove the default nullptr
value as there's no use in having an Object
with no data
.) Likewise with QuadTree
. Doing this eliminates a huge class of bugs that crop up from badly initialized instances of objects.
Performance
I ran a profile of inserting 100,000 objects into a QuadTree
. The slowest part (84% of runtime in my experiment) is the call to vector::erase()
. The reason is that this causes the rest of the elements in the vector to be moved back 1 spot. It ends up being a bunch of calls to memmove()
. It might be worth trying other containers instead of std::vector
. One that has O(1) removal would fix this problem. Or, you could swap the object that you're currently working on with the last object in the vector
and then erase only the last object. Doing this removed insert()
from even showing up in my traces. (I didn't do much verification of the resulting QuadTree
because there are no tests here, and no examples of how to use it, so make sure you do a lot of tests before going with that method.) It looked like this:
// Re-insert objects into new quadrant
for (unsigned i = 0; i < _objects.size();)
Object *object = _objects[i];
if (QuadTree *child = getChild(object->_bound))
std::swap(*(_objects.begin() + i), *(_objects.end() - 1));
_objects.erase(_objects.end() - 1);
object->_qt = nullptr;
child->insert(object);
else ++i;
stackoverflow.com/q/347441/5416291 there's a term for that.
â bruglesco
May 22 at 13:52
Thank you! I'm in the process of renaming most of the program now. I thought thatcontains()
would be a useful since an object'sqt
isn't meant to be accessed by anything other than theQuadTree
itself, so anyone who would like to check can just use that instead of the if statement. Also the reason why I had default constructors for every class is so I could initialize them later before I insert them into the quadtree.. but I see now that it was a bad idea haha. I'll make sure that isn't possible.
â m-byte
May 22 at 20:06
Also whyinsert
was slow makes a lot of sense now. I didn't realize how muchvector::erase()
was bottle necking my code until I actually thought about what it was doing. I'll try your method and see if it holds up, if not Ill try your suggestion of using another container. Which one would you suggest would work best in this scenario?
â m-byte
May 22 at 20:18
My first thought was to use astd::set
instead ofstd::vector
, since you don't care about the order of the elements. But there's a tradeoff in that insertions are O(log(n)). If the above swap and delete works, I'd just keep that.
â user1118321
May 23 at 3:22
add a comment |Â
up vote
3
down vote
Quadtrees are very useful, so this is a good exercise to implement your own. I think you've done a reasonable job here, but I see some things that could be improved.
Naming
Always name all of your parameters in your header. It makes it so much easier for another programmer that's trying to call the methods in a class if the parameters have useful, descriptive names. For example, in the Object
constructor, what is the AABB
parameter? Is it the bounds of the object? Is it the bounds of the quad in a QuadTree
in which the object is found? Please clarify!
Speaking of the Object
class, the name Object
is meaningless. Every single thing in your program (other than PODT) are objects, yet they can't all be inserted into a QuadTree
. Usually quad trees are used for things like collision-detection or finding the distance to neighbors. I would rename the Object
class to Collidable
or something along those lines.
What is the difference between contains()
, search()
and query()
? Just looking at the names, they all sound like the same thing. You should give them more distinct names that clarify what they actually do. Looking at the code, it seems that contains()
does a shallow search of the object's QuadTree
for the existence of the object. (Isn't that a tautology? Shouldn't you never be allowed to have an object's _qt
be a QuadTree
to which it does not belong?) search()
calls a callback for every object in a bounding box. So maybe a better name would be performTaskOnObjectsInRegion()
or something like that. And query()
appears to collect all objects within a bounding box, so I'd call it collectObjectsInRegion()
or something along those lines.
Default Constructors
All 3 of your classes have default constructors that do nothing and leave their member variables in an undefined state. Why? It would be better to disallow creating these objects in an unusable state. Instead of an axis-aligned bounding box that contains random (possibly nonsensical) values for its bounds, simply disallow creating one without bounds by using:
AABB() = delete;
This removes the default constructor and solves the problem of an invalid object being created. Likewise, if you do the same for Object
, then you never need for its _bound
to be invalid. You can then also remove the setData()
method as its data will always be set in the constructor. (I'd also remove the default nullptr
value as there's no use in having an Object
with no data
.) Likewise with QuadTree
. Doing this eliminates a huge class of bugs that crop up from badly initialized instances of objects.
Performance
I ran a profile of inserting 100,000 objects into a QuadTree
. The slowest part (84% of runtime in my experiment) is the call to vector::erase()
. The reason is that this causes the rest of the elements in the vector to be moved back 1 spot. It ends up being a bunch of calls to memmove()
. It might be worth trying other containers instead of std::vector
. One that has O(1) removal would fix this problem. Or, you could swap the object that you're currently working on with the last object in the vector
and then erase only the last object. Doing this removed insert()
from even showing up in my traces. (I didn't do much verification of the resulting QuadTree
because there are no tests here, and no examples of how to use it, so make sure you do a lot of tests before going with that method.) It looked like this:
// Re-insert objects into new quadrant
for (unsigned i = 0; i < _objects.size();)
Object *object = _objects[i];
if (QuadTree *child = getChild(object->_bound))
std::swap(*(_objects.begin() + i), *(_objects.end() - 1));
_objects.erase(_objects.end() - 1);
object->_qt = nullptr;
child->insert(object);
else ++i;
stackoverflow.com/q/347441/5416291 there's a term for that.
â bruglesco
May 22 at 13:52
Thank you! I'm in the process of renaming most of the program now. I thought thatcontains()
would be a useful since an object'sqt
isn't meant to be accessed by anything other than theQuadTree
itself, so anyone who would like to check can just use that instead of the if statement. Also the reason why I had default constructors for every class is so I could initialize them later before I insert them into the quadtree.. but I see now that it was a bad idea haha. I'll make sure that isn't possible.
â m-byte
May 22 at 20:06
Also whyinsert
was slow makes a lot of sense now. I didn't realize how muchvector::erase()
was bottle necking my code until I actually thought about what it was doing. I'll try your method and see if it holds up, if not Ill try your suggestion of using another container. Which one would you suggest would work best in this scenario?
â m-byte
May 22 at 20:18
My first thought was to use astd::set
instead ofstd::vector
, since you don't care about the order of the elements. But there's a tradeoff in that insertions are O(log(n)). If the above swap and delete works, I'd just keep that.
â user1118321
May 23 at 3:22
add a comment |Â
up vote
3
down vote
up vote
3
down vote
Quadtrees are very useful, so this is a good exercise to implement your own. I think you've done a reasonable job here, but I see some things that could be improved.
Naming
Always name all of your parameters in your header. It makes it so much easier for another programmer that's trying to call the methods in a class if the parameters have useful, descriptive names. For example, in the Object
constructor, what is the AABB
parameter? Is it the bounds of the object? Is it the bounds of the quad in a QuadTree
in which the object is found? Please clarify!
Speaking of the Object
class, the name Object
is meaningless. Every single thing in your program (other than PODT) are objects, yet they can't all be inserted into a QuadTree
. Usually quad trees are used for things like collision-detection or finding the distance to neighbors. I would rename the Object
class to Collidable
or something along those lines.
What is the difference between contains()
, search()
and query()
? Just looking at the names, they all sound like the same thing. You should give them more distinct names that clarify what they actually do. Looking at the code, it seems that contains()
does a shallow search of the object's QuadTree
for the existence of the object. (Isn't that a tautology? Shouldn't you never be allowed to have an object's _qt
be a QuadTree
to which it does not belong?) search()
calls a callback for every object in a bounding box. So maybe a better name would be performTaskOnObjectsInRegion()
or something like that. And query()
appears to collect all objects within a bounding box, so I'd call it collectObjectsInRegion()
or something along those lines.
Default Constructors
All 3 of your classes have default constructors that do nothing and leave their member variables in an undefined state. Why? It would be better to disallow creating these objects in an unusable state. Instead of an axis-aligned bounding box that contains random (possibly nonsensical) values for its bounds, simply disallow creating one without bounds by using:
AABB() = delete;
This removes the default constructor and solves the problem of an invalid object being created. Likewise, if you do the same for Object
, then you never need for its _bound
to be invalid. You can then also remove the setData()
method as its data will always be set in the constructor. (I'd also remove the default nullptr
value as there's no use in having an Object
with no data
.) Likewise with QuadTree
. Doing this eliminates a huge class of bugs that crop up from badly initialized instances of objects.
Performance
I ran a profile of inserting 100,000 objects into a QuadTree
. The slowest part (84% of runtime in my experiment) is the call to vector::erase()
. The reason is that this causes the rest of the elements in the vector to be moved back 1 spot. It ends up being a bunch of calls to memmove()
. It might be worth trying other containers instead of std::vector
. One that has O(1) removal would fix this problem. Or, you could swap the object that you're currently working on with the last object in the vector
and then erase only the last object. Doing this removed insert()
from even showing up in my traces. (I didn't do much verification of the resulting QuadTree
because there are no tests here, and no examples of how to use it, so make sure you do a lot of tests before going with that method.) It looked like this:
// Re-insert objects into new quadrant
for (unsigned i = 0; i < _objects.size();)
Object *object = _objects[i];
if (QuadTree *child = getChild(object->_bound))
std::swap(*(_objects.begin() + i), *(_objects.end() - 1));
_objects.erase(_objects.end() - 1);
object->_qt = nullptr;
child->insert(object);
else ++i;
Quadtrees are very useful, so this is a good exercise to implement your own. I think you've done a reasonable job here, but I see some things that could be improved.
Naming
Always name all of your parameters in your header. It makes it so much easier for another programmer that's trying to call the methods in a class if the parameters have useful, descriptive names. For example, in the Object
constructor, what is the AABB
parameter? Is it the bounds of the object? Is it the bounds of the quad in a QuadTree
in which the object is found? Please clarify!
Speaking of the Object
class, the name Object
is meaningless. Every single thing in your program (other than PODT) are objects, yet they can't all be inserted into a QuadTree
. Usually quad trees are used for things like collision-detection or finding the distance to neighbors. I would rename the Object
class to Collidable
or something along those lines.
What is the difference between contains()
, search()
and query()
? Just looking at the names, they all sound like the same thing. You should give them more distinct names that clarify what they actually do. Looking at the code, it seems that contains()
does a shallow search of the object's QuadTree
for the existence of the object. (Isn't that a tautology? Shouldn't you never be allowed to have an object's _qt
be a QuadTree
to which it does not belong?) search()
calls a callback for every object in a bounding box. So maybe a better name would be performTaskOnObjectsInRegion()
or something like that. And query()
appears to collect all objects within a bounding box, so I'd call it collectObjectsInRegion()
or something along those lines.
Default Constructors
All 3 of your classes have default constructors that do nothing and leave their member variables in an undefined state. Why? It would be better to disallow creating these objects in an unusable state. Instead of an axis-aligned bounding box that contains random (possibly nonsensical) values for its bounds, simply disallow creating one without bounds by using:
AABB() = delete;
This removes the default constructor and solves the problem of an invalid object being created. Likewise, if you do the same for Object
, then you never need for its _bound
to be invalid. You can then also remove the setData()
method as its data will always be set in the constructor. (I'd also remove the default nullptr
value as there's no use in having an Object
with no data
.) Likewise with QuadTree
. Doing this eliminates a huge class of bugs that crop up from badly initialized instances of objects.
Performance
I ran a profile of inserting 100,000 objects into a QuadTree
. The slowest part (84% of runtime in my experiment) is the call to vector::erase()
. The reason is that this causes the rest of the elements in the vector to be moved back 1 spot. It ends up being a bunch of calls to memmove()
. It might be worth trying other containers instead of std::vector
. One that has O(1) removal would fix this problem. Or, you could swap the object that you're currently working on with the last object in the vector
and then erase only the last object. Doing this removed insert()
from even showing up in my traces. (I didn't do much verification of the resulting QuadTree
because there are no tests here, and no examples of how to use it, so make sure you do a lot of tests before going with that method.) It looked like this:
// Re-insert objects into new quadrant
for (unsigned i = 0; i < _objects.size();)
Object *object = _objects[i];
if (QuadTree *child = getChild(object->_bound))
std::swap(*(_objects.begin() + i), *(_objects.end() - 1));
_objects.erase(_objects.end() - 1);
object->_qt = nullptr;
child->insert(object);
else ++i;
answered May 22 at 5:08
user1118321
10.1k11144
10.1k11144
stackoverflow.com/q/347441/5416291 there's a term for that.
â bruglesco
May 22 at 13:52
Thank you! I'm in the process of renaming most of the program now. I thought thatcontains()
would be a useful since an object'sqt
isn't meant to be accessed by anything other than theQuadTree
itself, so anyone who would like to check can just use that instead of the if statement. Also the reason why I had default constructors for every class is so I could initialize them later before I insert them into the quadtree.. but I see now that it was a bad idea haha. I'll make sure that isn't possible.
â m-byte
May 22 at 20:06
Also whyinsert
was slow makes a lot of sense now. I didn't realize how muchvector::erase()
was bottle necking my code until I actually thought about what it was doing. I'll try your method and see if it holds up, if not Ill try your suggestion of using another container. Which one would you suggest would work best in this scenario?
â m-byte
May 22 at 20:18
My first thought was to use astd::set
instead ofstd::vector
, since you don't care about the order of the elements. But there's a tradeoff in that insertions are O(log(n)). If the above swap and delete works, I'd just keep that.
â user1118321
May 23 at 3:22
add a comment |Â
stackoverflow.com/q/347441/5416291 there's a term for that.
â bruglesco
May 22 at 13:52
Thank you! I'm in the process of renaming most of the program now. I thought thatcontains()
would be a useful since an object'sqt
isn't meant to be accessed by anything other than theQuadTree
itself, so anyone who would like to check can just use that instead of the if statement. Also the reason why I had default constructors for every class is so I could initialize them later before I insert them into the quadtree.. but I see now that it was a bad idea haha. I'll make sure that isn't possible.
â m-byte
May 22 at 20:06
Also whyinsert
was slow makes a lot of sense now. I didn't realize how muchvector::erase()
was bottle necking my code until I actually thought about what it was doing. I'll try your method and see if it holds up, if not Ill try your suggestion of using another container. Which one would you suggest would work best in this scenario?
â m-byte
May 22 at 20:18
My first thought was to use astd::set
instead ofstd::vector
, since you don't care about the order of the elements. But there's a tradeoff in that insertions are O(log(n)). If the above swap and delete works, I'd just keep that.
â user1118321
May 23 at 3:22
stackoverflow.com/q/347441/5416291 there's a term for that.
â bruglesco
May 22 at 13:52
stackoverflow.com/q/347441/5416291 there's a term for that.
â bruglesco
May 22 at 13:52
Thank you! I'm in the process of renaming most of the program now. I thought that
contains()
would be a useful since an object's qt
isn't meant to be accessed by anything other than the QuadTree
itself, so anyone who would like to check can just use that instead of the if statement. Also the reason why I had default constructors for every class is so I could initialize them later before I insert them into the quadtree.. but I see now that it was a bad idea haha. I'll make sure that isn't possible.â m-byte
May 22 at 20:06
Thank you! I'm in the process of renaming most of the program now. I thought that
contains()
would be a useful since an object's qt
isn't meant to be accessed by anything other than the QuadTree
itself, so anyone who would like to check can just use that instead of the if statement. Also the reason why I had default constructors for every class is so I could initialize them later before I insert them into the quadtree.. but I see now that it was a bad idea haha. I'll make sure that isn't possible.â m-byte
May 22 at 20:06
Also why
insert
was slow makes a lot of sense now. I didn't realize how muchvector::erase()
was bottle necking my code until I actually thought about what it was doing. I'll try your method and see if it holds up, if not Ill try your suggestion of using another container. Which one would you suggest would work best in this scenario?â m-byte
May 22 at 20:18
Also why
insert
was slow makes a lot of sense now. I didn't realize how muchvector::erase()
was bottle necking my code until I actually thought about what it was doing. I'll try your method and see if it holds up, if not Ill try your suggestion of using another container. Which one would you suggest would work best in this scenario?â m-byte
May 22 at 20:18
My first thought was to use a
std::set
instead of std::vector
, since you don't care about the order of the elements. But there's a tradeoff in that insertions are O(log(n)). If the above swap and delete works, I'd just keep that.â user1118321
May 23 at 3:22
My first thought was to use a
std::set
instead of std::vector
, since you don't care about the order of the elements. But there's a tradeoff in that insertions are O(log(n)). If the above swap and delete works, I'd just keep that.â user1118321
May 23 at 3:22
add a comment |Â
up vote
1
down vote
Some things I haven't seen in the previous reviews, or I just plain missed ...
Quadtree
With moving object the quadtree might not be your best choice, at best an object will move from one child into the next but at worst you could have to delete a whole bunch as the path that the item is on might become empty and you have to remove all the children. Having multiple objects per node also forces you to keep a dynamic data structure in the node, that is not ideal. An alternative solution is the AABB Tree, (http://www.randygaul.net/2013/08/06/dynamic-aabb-tree/) has a good description. As this tree only has one object per leaf at max and has a max number of nodes you can preallocate the memory and you can use a linear store for all the nodes. Additionally it is possible to change the object AABBs and update the structure with new bounding boxes. If updating the tree usually you'll check for an amount of change that will trigger a rebuild.
Depending on the number of objects, you might just be quicker doing a pairwise AABB test, did you do the timing ?
Allocations
Each insertion and removal incurs a good number of deallocations and allocations, if you want to stick with the quad-tree you can save on allocations by preserving children that you have already allocated and only allocating when needed.
update()
Your update calls remove()
and insert()
which updates the array twice, that is not optimal. If you're doing collision detection, in most cases you'll probably have a bunch of updates to do at the same time. If you can utilize this you might be able to improve your performance.
Do you really need it ?
_maxLevel
and_level
are never read in the code, do you really need it ?contains()
are you sure you ever need to test if a specific object is in the tree ? Meaning are you ever in a state where there are objects that might be in the tree ? If you can get rid of that function, you can get rid of the trackingstd::vector<>
the maintainance of which cost a lot of time.
_children
_children
could be a std::array
rather than a c-array;
Formatting
I'm not a fan of use of braces like this
if (!_isLeaf)
if (QuadTree *child = getChild(obj->_bound))
return child->insert(obj);
What made you put the first pair of brace in ? Just because it looks complicated ? A rule of thumb I use is, "you can skip the braces if it fits in one line, e.g.
if (obj->_qt != nullptr) return false;
Naming
contains
, query
, search
, any
do very similar things, contains
, getObjectsInBound
, search
could be labeled as forEachObjectInBound
(similar to std::for_each
with bounds), and any is findEachObjectInBoundIf
(similar to std::find_if
with bounds)
Consider std::algorithm
Your trio of query
, search
and any
could be replaced by always using the following search()
void QuadTree::query(const AABB &bound, std::vector<Object*> &returnObjects) const
if (!_isLeaf)
if (QuadTree *child = getChild(bound))
child->search(bound, returnObjects);
else
for (auto&& node : _children)
if (node->_bound.intersects(bound))
node->search(bound, returnObjects);
// Now search objects
for (auto&& obj : _objects)
if (obj->_bound.intersects(bound))
returnObjects.push_back(obj);
And then use std::foreach
or std::find_if
on it. Although if you use any()
a lot you would want to implement it in a way it exits as sound as the condition is satisfied, your implementation finds all objects no matter if the condition is true early.
add a comment |Â
up vote
1
down vote
Some things I haven't seen in the previous reviews, or I just plain missed ...
Quadtree
With moving object the quadtree might not be your best choice, at best an object will move from one child into the next but at worst you could have to delete a whole bunch as the path that the item is on might become empty and you have to remove all the children. Having multiple objects per node also forces you to keep a dynamic data structure in the node, that is not ideal. An alternative solution is the AABB Tree, (http://www.randygaul.net/2013/08/06/dynamic-aabb-tree/) has a good description. As this tree only has one object per leaf at max and has a max number of nodes you can preallocate the memory and you can use a linear store for all the nodes. Additionally it is possible to change the object AABBs and update the structure with new bounding boxes. If updating the tree usually you'll check for an amount of change that will trigger a rebuild.
Depending on the number of objects, you might just be quicker doing a pairwise AABB test, did you do the timing ?
Allocations
Each insertion and removal incurs a good number of deallocations and allocations, if you want to stick with the quad-tree you can save on allocations by preserving children that you have already allocated and only allocating when needed.
update()
Your update calls remove()
and insert()
which updates the array twice, that is not optimal. If you're doing collision detection, in most cases you'll probably have a bunch of updates to do at the same time. If you can utilize this you might be able to improve your performance.
Do you really need it ?
_maxLevel
and_level
are never read in the code, do you really need it ?contains()
are you sure you ever need to test if a specific object is in the tree ? Meaning are you ever in a state where there are objects that might be in the tree ? If you can get rid of that function, you can get rid of the trackingstd::vector<>
the maintainance of which cost a lot of time.
_children
_children
could be a std::array
rather than a c-array;
Formatting
I'm not a fan of use of braces like this
if (!_isLeaf)
if (QuadTree *child = getChild(obj->_bound))
return child->insert(obj);
What made you put the first pair of brace in ? Just because it looks complicated ? A rule of thumb I use is, "you can skip the braces if it fits in one line, e.g.
if (obj->_qt != nullptr) return false;
Naming
contains
, query
, search
, any
do very similar things, contains
, getObjectsInBound
, search
could be labeled as forEachObjectInBound
(similar to std::for_each
with bounds), and any is findEachObjectInBoundIf
(similar to std::find_if
with bounds)
Consider std::algorithm
Your trio of query
, search
and any
could be replaced by always using the following search()
void QuadTree::query(const AABB &bound, std::vector<Object*> &returnObjects) const
if (!_isLeaf)
if (QuadTree *child = getChild(bound))
child->search(bound, returnObjects);
else
for (auto&& node : _children)
if (node->_bound.intersects(bound))
node->search(bound, returnObjects);
// Now search objects
for (auto&& obj : _objects)
if (obj->_bound.intersects(bound))
returnObjects.push_back(obj);
And then use std::foreach
or std::find_if
on it. Although if you use any()
a lot you would want to implement it in a way it exits as sound as the condition is satisfied, your implementation finds all objects no matter if the condition is true early.
add a comment |Â
up vote
1
down vote
up vote
1
down vote
Some things I haven't seen in the previous reviews, or I just plain missed ...
Quadtree
With moving object the quadtree might not be your best choice, at best an object will move from one child into the next but at worst you could have to delete a whole bunch as the path that the item is on might become empty and you have to remove all the children. Having multiple objects per node also forces you to keep a dynamic data structure in the node, that is not ideal. An alternative solution is the AABB Tree, (http://www.randygaul.net/2013/08/06/dynamic-aabb-tree/) has a good description. As this tree only has one object per leaf at max and has a max number of nodes you can preallocate the memory and you can use a linear store for all the nodes. Additionally it is possible to change the object AABBs and update the structure with new bounding boxes. If updating the tree usually you'll check for an amount of change that will trigger a rebuild.
Depending on the number of objects, you might just be quicker doing a pairwise AABB test, did you do the timing ?
Allocations
Each insertion and removal incurs a good number of deallocations and allocations, if you want to stick with the quad-tree you can save on allocations by preserving children that you have already allocated and only allocating when needed.
update()
Your update calls remove()
and insert()
which updates the array twice, that is not optimal. If you're doing collision detection, in most cases you'll probably have a bunch of updates to do at the same time. If you can utilize this you might be able to improve your performance.
Do you really need it ?
_maxLevel
and_level
are never read in the code, do you really need it ?contains()
are you sure you ever need to test if a specific object is in the tree ? Meaning are you ever in a state where there are objects that might be in the tree ? If you can get rid of that function, you can get rid of the trackingstd::vector<>
the maintainance of which cost a lot of time.
_children
_children
could be a std::array
rather than a c-array;
Formatting
I'm not a fan of use of braces like this
if (!_isLeaf)
if (QuadTree *child = getChild(obj->_bound))
return child->insert(obj);
What made you put the first pair of brace in ? Just because it looks complicated ? A rule of thumb I use is, "you can skip the braces if it fits in one line, e.g.
if (obj->_qt != nullptr) return false;
Naming
contains
, query
, search
, any
do very similar things, contains
, getObjectsInBound
, search
could be labeled as forEachObjectInBound
(similar to std::for_each
with bounds), and any is findEachObjectInBoundIf
(similar to std::find_if
with bounds)
Consider std::algorithm
Your trio of query
, search
and any
could be replaced by always using the following search()
void QuadTree::query(const AABB &bound, std::vector<Object*> &returnObjects) const
if (!_isLeaf)
if (QuadTree *child = getChild(bound))
child->search(bound, returnObjects);
else
for (auto&& node : _children)
if (node->_bound.intersects(bound))
node->search(bound, returnObjects);
// Now search objects
for (auto&& obj : _objects)
if (obj->_bound.intersects(bound))
returnObjects.push_back(obj);
And then use std::foreach
or std::find_if
on it. Although if you use any()
a lot you would want to implement it in a way it exits as sound as the condition is satisfied, your implementation finds all objects no matter if the condition is true early.
Some things I haven't seen in the previous reviews, or I just plain missed ...
Quadtree
With moving object the quadtree might not be your best choice, at best an object will move from one child into the next but at worst you could have to delete a whole bunch as the path that the item is on might become empty and you have to remove all the children. Having multiple objects per node also forces you to keep a dynamic data structure in the node, that is not ideal. An alternative solution is the AABB Tree, (http://www.randygaul.net/2013/08/06/dynamic-aabb-tree/) has a good description. As this tree only has one object per leaf at max and has a max number of nodes you can preallocate the memory and you can use a linear store for all the nodes. Additionally it is possible to change the object AABBs and update the structure with new bounding boxes. If updating the tree usually you'll check for an amount of change that will trigger a rebuild.
Depending on the number of objects, you might just be quicker doing a pairwise AABB test, did you do the timing ?
Allocations
Each insertion and removal incurs a good number of deallocations and allocations, if you want to stick with the quad-tree you can save on allocations by preserving children that you have already allocated and only allocating when needed.
update()
Your update calls remove()
and insert()
which updates the array twice, that is not optimal. If you're doing collision detection, in most cases you'll probably have a bunch of updates to do at the same time. If you can utilize this you might be able to improve your performance.
Do you really need it ?
_maxLevel
and_level
are never read in the code, do you really need it ?contains()
are you sure you ever need to test if a specific object is in the tree ? Meaning are you ever in a state where there are objects that might be in the tree ? If you can get rid of that function, you can get rid of the trackingstd::vector<>
the maintainance of which cost a lot of time.
_children
_children
could be a std::array
rather than a c-array;
Formatting
I'm not a fan of use of braces like this
if (!_isLeaf)
if (QuadTree *child = getChild(obj->_bound))
return child->insert(obj);
What made you put the first pair of brace in ? Just because it looks complicated ? A rule of thumb I use is, "you can skip the braces if it fits in one line, e.g.
if (obj->_qt != nullptr) return false;
Naming
contains
, query
, search
, any
do very similar things, contains
, getObjectsInBound
, search
could be labeled as forEachObjectInBound
(similar to std::for_each
with bounds), and any is findEachObjectInBoundIf
(similar to std::find_if
with bounds)
Consider std::algorithm
Your trio of query
, search
and any
could be replaced by always using the following search()
void QuadTree::query(const AABB &bound, std::vector<Object*> &returnObjects) const
if (!_isLeaf)
if (QuadTree *child = getChild(bound))
child->search(bound, returnObjects);
else
for (auto&& node : _children)
if (node->_bound.intersects(bound))
node->search(bound, returnObjects);
// Now search objects
for (auto&& obj : _objects)
if (obj->_bound.intersects(bound))
returnObjects.push_back(obj);
And then use std::foreach
or std::find_if
on it. Although if you use any()
a lot you would want to implement it in a way it exits as sound as the condition is satisfied, your implementation finds all objects no matter if the condition is true early.
answered May 23 at 17:45
Harald Scheirich
1,171418
1,171418
add a comment |Â
add a comment |Â
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f194836%2frecursive-quadtree-implementation-in-c%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
I'm a little confused reading this. If my root-level
QuadTree
has bounds from, say, -1 to +1 in both directions, it will be subdivided into 4 quads from 0 to ±1, etc. If I have an object that's centered at the origin, it looks to me like it will only end up in one of the second-level quads, not all 4, which is where it needs to be. Am I misunderstanding that?â user1118321
May 21 at 3:00
It depends on the size of the object. It will only be re-inserted into one of the second-level quadrants if its boundaries can fit inside of one (aka if
getChild
does not return nullptr), otherwise, it is skipped and remains inside all four quadrants. I should probably make that more clear :Pâ m-byte
May 21 at 3:51
remember to upvote helpful answers!
â JDà Âugosz
May 23 at 0:46
What are you using this quadtree for ? collision detection, finding closest pairs ??
â Harald Scheirich
May 23 at 1:01
@HaraldScheirich I'm using it mainly for collision detection.
â m-byte
May 23 at 1:40