I want a data-structure that supports these specific 1:N relations :-
1#. Human
raise 0-N Human
2#. Human
has 0-N Dog
3#. Human
cultivate 0-N Tree
4#. Dog
is a house of 0-N Parasites
.
Note:
- State in these relations are all temporary e.g. Human1
may raise Human2
, but after a year, Human1
may abandon Human2
.
- All objects are inherited from BaseObject
and has unique int ID.
In all of the above relation, I want to be able to support these features :-
F1. add relation e.g. human_dog->addRelation(Human* a,Dog* b)
F2. remove relation e.g. human_dog->removeRelation(Human* a,Dog* b)
F3. query all children e.g. human_dog->getAllChildren(Human*)
F4. query all parent e.g. human_dog->getAllParents(Dog*)
F5. check whether a parent has >=1 child
F6. check whether a child has >=1 parent
F7. remove all children for a parent
F8. remove all parent for a child
This can be implemented by std::unordered_map
or something more customized quite easily.
Here comes the hard part
I want to mark relation 1#,2#,3# (i.e. all solid lines) as Feed.
It has to support feature F3-F8 in an aggregating style.
For example :-
feed->getAllChildren(BaseObject* b)
:
If b
is human, it must return all children of raise,has and cultivate of the b
.
feed->removeAllParent(BaseObject* b)
:
If b
is a dog, it will effect like cultivate->removeAllParent(b)
.
In summary, I want to be able to easily inject such aggregation.
Ex. It is useful to call :-
void BaseObject::declareForFreedom(){
feed->removeAllParent(this);
}
The above example shows only 4 relations and 1 level of indirection.
In my real case, there are 8-10 relations and 3-4 levels of such inherit/indirection.
Question
What is a data-structure/design-pattern that suitable for this case?
I currently create a custom 1:N relation for 1#-4#, and hard-code every feed's function. It is tedious.
I have banged by head for a few months, but not found any implementation that look elegant.
Demo
http://coliru.stacked-crooked.com/a/1f2decd7a8d96e3c
Basic type:-
#include <iostream>
#include <map>
#include <vector>
enum class Type{
HUMAN,DOG,TREE,PARASITE,ERROR
}; //for simplicity
class BaseObject{public: Type type=Type::ERROR; };
class Human : public BaseObject{
public: Human(){ type=Type::HUMAN; }
};
class Dog : public BaseObject{
public: Dog(){ type=Type::DOG; }
};
class Tree : public BaseObject{
public: Tree(){ type=Type::TREE; }
};
class Parasite : public BaseObject{
public: Parasite(){ type=Type::PARASITE; }
};
Basic 1:N map
template<class A,class B> class MapSimple{
std::multimap<A*, B*> aToB;
std::multimap<B*, A*> bToA;
public: void addRelation(A* b1,B* b2){
aToB.insert ( std::pair<A*,B*>(b1,b2) );
bToA.insert ( std::pair<B*,A*>(b2,b1) );
}
public: std::vector<B*> queryAllChildren(A* b1){
auto ret = aToB.equal_range(b1);
auto result=std::vector<B*>();
for (auto it=ret.first; it!=ret.second; ++it){
result.push_back(it->second);
}
return result;
}
public: void removeAllParent(B* b){
if(bToA.count(b)==0)return;
A* a=bToA.find(b)->second;
bToA.erase(b);
auto iterpair = aToB.equal_range(a);
auto it = iterpair.first;
for (; it != iterpair.second; ++it) {
if (it->second == b) {
aToB.erase(it);
break;
}
}
}
//.. other functions
};
Here is the database instance and the aggregation :-
MapSimple<Human,Human> raise;
MapSimple<Human,Dog> has;
MapSimple<Human,Tree> cultivate;
MapSimple<Dog,Parasite> isHouseOf;
class Feed{
public: void removeAllParent(BaseObject* b1){
if(b1->type==Type::HUMAN){
raise.removeAllParent(static_cast<Human*>(b1));
}
if(b1->type==Type::DOG){
has.removeAllParent(static_cast<Dog*>(b1));
}
//.... some other condition (I have to hard code them - tedious) ...
}
//other function
};
Feed feed;
Usage
int main(){
Human h1;
Dog d1,d2;
has.addRelation(&h1,&d1);
has.addRelation(&h1,&d2);
auto result=has.queryAllChildren(&h1);
std::cout<<result.size(); //print 2
feed.removeAllParent(&d1);
result=has.queryAllChildren(&h1);
std::cout<<result.size(); //print 1
}
What's wrong with the straight-forward implementation?
E.g.:
BaseObject.hpp
#include <vector>
template<class T>
using prtVector = std::vector<T*>;
class BaseObject {
public:
virtual prtVector<BaseObject> getAllParents() const = 0;
virtual prtVector<BaseObject> getAllChilderen() const = 0;
virtual void removeAllParents() = 0;
virtual void removeAllChildren() = 0;
};
Human.hpp
#include "BaseObject.hpp"
#include "Tree.hpp"
#include "Dog.hpp"
class Tree;
class Dog;
class Human : public BaseObject {
public:
prtVector<BaseObject> getAllParents() const override;
prtVector<BaseObject> getAllChildren() const override;
void removeAllParents() override;
void removeAllChildren() override ;
friend class Dog;
friend class Tree;
template<class A, class B>
friend void addRelation(A* a, B* b);
private:
void addParent(Human* const);
void removeParent(Human const* const);
void addChild(Human* const);
void removeChild(Human const* const);
void addChild(Tree* const);
void removeChild(Tree const* const);
void addChild(Dog* const);
void removeChild(Dog const* const);
private:
prtVector<Human> parents;
prtVector<Human> children;
prtVector<Tree> plants;
prtVector<Dog> pets;
};
Human.cpp
#include "Human.hpp"
prtVector<BaseObject> Human::getAllParents() const {
prtVector<BaseObject> result(std::cbegin(parents), std::cend(parents));
return result;
}
prtVector<BaseObject> Human::getAllChildren() const {
prtVector<BaseObject> result(std::cbegin(children), std::cend(children));
result.insert(std::end(result), std::cbegin(pets), std::cend(pets));
result.insert(std::end(result), std::cbegin(plants), std::cend(plants));
return result;
}
void Human::removeAllParents() {
for (auto parent : parents) { parent->removeChild(this); }
parents.clear();
}
void Human::removeAllChildren() {
for (auto child : children) { child->removeParent(this); } children.clear();
for (auto pet : pets) { pet->removeParent(this); } pets.clear();
for (auto plant : plants) { plant->removeParent(this); } plants.clear();
}
void Human::addParent(Human* const parent) { parents.push_back(parent); }
#include <algorithm>
void Human::removeParent(Human const* const parent) {
auto it = std::find(std::cbegin(parents), std::cend(parents), parent);
if (it != std::cend(parents)) parents.erase(it);
}
void Human::addChild(Human* const child) { children.push_back(child); }
etc, etc...
Same for other types....
main.cpp
#include "Human.hpp"
#include "Dog.hpp"
template<class A, class B>
void addRelation(A* a, B* b)
{
a->addChild(b);
b->addParent(a);
}
template<class A>
prtVector<BaseObject> queryAllChildren(A* obj)
{
return obj->getAllChilderen();
}
template<class A>
void removeAllParents(A* obj)
{
obj->removeAllParents();
}
#include <iostream>
int main() {
Human h1;
Dog d1, d2;
addRelation(&h1, &d1);
addRelation(&h1, &d2);
auto result = queryAllChildren(&h1);
std::cout << result.size() << "\n"; //print 2
removeAllParents(&d1);
result = queryAllChildren(&h1);
std::cout << result.size() << "\n"; //print 1
std::cin.ignore();
}
IMHO this gives readable and maintainable code. Can probably be optimized somewhat. But at least the relationships are very clear from the code.
EDIT
Better code was suggested by Jarod42 in this topic. C++17 style:
#include <algorithm>
#include <tuple>
#include <vector>
class BaseObject {
public:
virtual ~BaseObject() = default;
virtual std::vector<BaseObject*> getAllParents() const = 0;
virtual std::vector<BaseObject*> getAllChildren() const = 0;
virtual void removeAllParents() = 0;
virtual void removeAllChildren() = 0;
};
template<typename TParentTuple, typename TChilderenTuple>
class Obj;
template<typename... ParentTags,
typename... ChildTags>
class Obj<std::tuple<ParentTags...>, std::tuple<ChildTags...>> : public BaseObject
{
std::tuple<std::vector<typename ParentTags::obj_type*>...> parents;
std::tuple<std::vector<typename ChildTags::obj_type*>...> children;
public:
template <typename T>
void addParent(T* parent) { std::get<std::vector<T*>>(parents).push_back(parent); }
template <typename T>
void removeParent(const T* parent) {
auto& v = std::get<std::vector<T*>>(parents);
auto it = std::find(std::cbegin(v), std::cend(v), parent);
if (it != std::cend(v)) { v.erase(it); }
}
template <typename T>
void addChild(T* child) { std::get<std::vector<T*>>(children).push_back(child); }
template <typename T>
void removeChild(const T* child) {
auto& v = std::get<std::vector<T*>>(children);
auto it = std::find(std::cbegin(v), std::cend(v), child);
if (it != std::cend(v)) { v.erase(it); }
}
std::vector<BaseObject*> getAllParents() const override {
std::vector<BaseObject*> res;
std::apply([&](auto&... v){ (res.insert(res.end(), v.begin(), v.end()), ...); },
parents);
return res;
}
std::vector<BaseObject*> getAllChildren() const override {
std::vector<BaseObject*> res;
std::apply([&](auto&... v){ (res.insert(res.end(), v.begin(), v.end()), ...); },
children);
return res;
}
void removeAllParents() override {
std::apply(
[this](auto&... v)
{
[[maybe_unused]] auto clean = [this](auto& v) {
for (auto* parent : v) {
parent->removeChild(this);
}
v.clear();
};
(clean(v), ...);
},
parents);
}
void removeAllChildren() override {
std::apply(
[this](auto&... v)
{
[[maybe_unused]] auto clean = [this](auto& v) {
for (auto* child : v) {
child->removeParent(this);
}
v.clear();
};
( clean(v), ...);
},
children);
}
};
struct Human_tag;
struct Tree_tag;
struct Dog_tag;
struct Parasite_tag;
using Human = Obj<std::tuple<>, std::tuple<Tree_tag, Dog_tag>>;
using Tree = Obj<std::tuple<Human_tag>, std::tuple<>>;
using Dog = Obj<std::tuple<Human_tag>, std::tuple<Parasite_tag>>;
using Parasite = Obj<std::tuple<Dog_tag>, std::tuple<>>;
struct Human_tag { using obj_type = Human; };
struct Tree_tag { using obj_type = Tree; };
struct Dog_tag { using obj_type = Dog; };
struct Parasite_tag { using obj_type = Parasite; };
template<class A, class B>
void addRelation(A* a, B* b)
{
a->addChild(b);
b->addParent(a);
}
#include <iostream>
int main() {
Human h1;
Dog d1, d2;
addRelation(&h1, &d1);
addRelation(&h1, &d2);
auto result = h1.getAllChildren();
std::cout << result.size() << "\n"; //print 2
d1.removeAllParents();
result = h1.getAllChildren();
std::cout << result.size() << "\n"; //print 1
}
Old code: (my attempt)
OK, since you did not want duplicated code, I've been using this project as my first experience with metaprogramming/variadic templating. So this is what I got:
#include <tuple>
#include <vector>
#include <algorithm>
template<class T>
using prtVector = std::vector<T*>;
// Interface, as required by assignment
class BaseObject {
public:
virtual ~BaseObject() {}
virtual prtVector<BaseObject> getAllParents() const = 0;
virtual prtVector<BaseObject> getAllChildren() const = 0;
virtual void removeAllParents() = 0;
virtual void removeAllChildren() = 0;
};
// base prototype
template<typename TOwnTag, typename TParentTagsTuple, typename TChildTagsTuple>
class Obj;
// Parent-type deduction
template<typename TOwnTag, typename TParentTag, typename... TParentTags, typename... TChildTags>
class Obj<TOwnTag, std::tuple<TParentTag, TParentTags...>, std::tuple<TChildTags...>>
: public Obj<TOwnTag, std::tuple<TParentTags...>, std::tuple<TChildTags...>>
{
// local types
using TOwn = typename TOwnTag::obj_type;
using TParent = typename TParentTag::obj_type;
// container
prtVector<TParent> parentsPtrs;
//befriend types
friend class Obj;
template<class A, class B>
friend void addRelation(A* const a, B* const b);
protected:
// prevent base function hiding with 'using'-declaration
using Obj<TOwnTag, std::tuple<TParentTags...>, std::tuple<TChildTags...>>::addParent;
using Obj<TOwnTag, std::tuple<TParentTags...>, std::tuple<TChildTags...>>::removeParent;
// add and remove element functions
void addParent(TParent* const parentPtr) { parentsPtrs.push_back(parentPtr); }
void removeParent(TParent const* const parentPtr) {
auto it = std::find(std::cbegin(parentsPtrs), std::cend(parentsPtrs), parentPtr);
if (it != std::cend(parentsPtrs)) parentsPtrs.erase(it);
}
public:
virtual ~Obj() {}
virtual prtVector<BaseObject> getAllParents() const override {
auto result = Obj<TOwnTag, std::tuple<TParentTags...>, std::tuple<TChildTags...>>::getAllParents();
result.insert(std::begin(result), std::cbegin(parentsPtrs), std::cend(parentsPtrs));
return result;
}
virtual prtVector<BaseObject> getAllChildren() const override {
return Obj<TOwnTag, std::tuple<TParentTags...>, std::tuple<TChildTags...>>::getAllChildren();
}
virtual void removeAllParents() override {
Obj<TOwnTag, std::tuple<TParentTags...>, std::tuple<TChildTags...>>::removeAllParents();
for (auto&& parent : parentsPtrs) parent->removeChild(reinterpret_cast<TOwn* const>(this));
}
virtual void removeAllChildren() override {
Obj<TOwnTag, std::tuple<TParentTags...>, std::tuple<TChildTags...>>::removeAllChildren();
}
};
// Child-type deduction
template<typename TOwnTag, typename TChildTag, typename... TChildTags>
class Obj<TOwnTag, std::tuple<>, std::tuple<TChildTag, TChildTags...>>
: public Obj<TOwnTag, std::tuple<>, std::tuple<TChildTags...>>
{
// local types
using TOwn = typename TOwnTag::obj_type;
using TChild = typename TChildTag::obj_type;
// container
prtVector<TChild> childrenPtrs;
//befriend types
friend class Obj;
template<class A, class B>
friend void addRelation(A* const a, B* const b);
protected:
// empty functions required for 'using'-declaration
void addParent() {}
void removeParent() {}
// prevent base function hiding with 'using'-declaration
using Obj<TOwnTag, std::tuple<>, std::tuple<TChildTags...>>::addChild;
using Obj<TOwnTag, std::tuple<>, std::tuple<TChildTags...>>::removeChild;
// add and remove element functions
void addChild(TChild* const childPtr) { childrenPtrs.push_back(childPtr); }
void removeChild(TChild const* const childPtr) {
auto it = std::find(std::cbegin(childrenPtrs), std::cend(childrenPtrs), childPtr);
if (it != std::cend(childrenPtrs)) childrenPtrs.erase(it);
}
public:
virtual ~Obj() {}
virtual prtVector<BaseObject> getAllParents() const override {
return Obj<TOwnTag, std::tuple<>, std::tuple<TChildTags...>>::getAllParents();
}
virtual prtVector<BaseObject> getAllChildren() const override {
auto result = Obj<TOwnTag, std::tuple<>, std::tuple<TChildTags...>>::getAllChildren();
result.insert(std::begin(result), std::cbegin(childrenPtrs), std::cend(childrenPtrs));
return result;
}
virtual void removeAllParents() override {}
virtual void removeAllChildren() override {
Obj<TOwnTag, std::tuple<>, std::tuple<TChildTags...>>::removeAllChildren();
for (auto&& child : childrenPtrs) child->removeParent(reinterpret_cast<TOwn* const>(this));
}
};
// terminator
template<typename TOwnTag>
class Obj<TOwnTag, std::tuple<>, std::tuple<>> : public BaseObject {
protected:
// empty functions required for 'using'-declaration
void addChild() {}
void removeChild() {}
void addParent() {}
void removeParent() {}
public:
virtual ~Obj() {}
virtual prtVector<BaseObject> getAllParents() const override {
return prtVector<BaseObject>();
}
virtual prtVector<BaseObject> getAllChildren() const override {
return prtVector<BaseObject>();
}
virtual void removeAllParents() override {}
virtual void removeAllChildren() override {}
};
//prototype class tags
struct Human_tag;
struct Tree_tag;
struct Dog_tag;
struct Parasite_tag;
//define class types
using Human = Obj<Human_tag, std::tuple<>, std::tuple<Tree_tag, Dog_tag>>;
using Tree = Obj<Tree_tag, std::tuple<Human_tag>, std::tuple<>>;
using Dog = Obj<Dog_tag, std::tuple<Human_tag>, std::tuple<Parasite_tag>>;
using Parasite = Obj<Parasite_tag, std::tuple<Dog_tag>, std::tuple<>>;
//couple tags to classes
struct Human_tag { using obj_type = Human; };
struct Tree_tag { using obj_type = Tree; };
struct Dog_tag { using obj_type = Dog; };
struct Parasite_tag { using obj_type = Parasite; };
//(befriend)helper function
// maybe could do somehting with std::enable_if
// i.e. "enable if type B is in child tuple of A and
// type A is in parent tuple of B"
// that way the parser will already detect a relation is not possible
template<class A, class B>
void addRelation(A* const a, B* const b)
{
a->addChild(b);
b->addParent(a);
}
// now for some testing
#include <iostream>
int main() {
Human h1;
Dog d1, d2;
Parasite p1;
addRelation(&h1, &d1);
addRelation(&h1, &d2);
addRelation(&d1, &p1);
//addRelation(&h1, &p1); // compiler error
auto result = h1.getAllChildren();
std::cout << result.size() << "\n"; //print 2
d1.removeAllParents();
result = h1.getAllChildren();
std::cout << result.size() << "\n"; //print 1
std::cin.ignore();
}
Please ask questions about anything that is unclear, because I've been learning so much new stuff over the past 24 hours, that I don't know where to begin with my explanation.