3

Let's say I have three classes, Solid, Face, and Edge that are defined as follows:

class Solid{
    public:
        // perform an action on a single edge.
        void addFillet(int edgeNum);
        // perform an action on a single face
        void addBore(int faceNum);
        // perform an action on all faces and edges
        void move(Pos newPosition);
    private:
        std::vector<Edge*> edges;
        std::vector<Face*> faces;
};

class Face{
    public:
        // will modify a subset of edges
        virtual void changeHeight(int newHeight) = 0;
    private:
        int myNum;
        std::vector<Edge> edges;
}

class Edge{
    public:
        virtual void changeLength(int newLength) = 0;
    private:
        int myNum;
        int length;
}

in this example, Solid manages a 'superset' of Edges. Each Face that Solid manages will have a 'sub-set' of Solid.edges. Further, any two Solid.faces may have a common Edge.

My question: are there any design patterns or general object-oriented principles for dealing with situations like this? How can I manage the relationship between Solid.edges and Face.edges? More specifically

wesanyer
  • 982
  • 1
  • 6
  • 27
  • 3
    Well typically you only store the Edge buffer in the Solid object, and for each Face you store an index to the Edge's it owns. (Drawing parallel with conventional computer rendering which stores vertices) – meowgoesthedog Aug 01 '17 at 13:13
  • 1
    As @meowgoesthedog mentioned this is similar to how OpenGL handles [Vertex Arrays](https://www.khronos.org/opengl/wiki/Vertex_Specification#Vertex_Array_Object). One object owns (and by that I mean handles allocation) the actual vertices, while the others refer to them by id. – Cory Kramer Aug 01 '17 at 13:16
  • 1
    You likely want to have a `shared_ptr` held in `Solid` and `Face`. Unless `Solid` is the ultimate owner, then you may want a `weak_ptr` in `Face`. – AndyG Aug 01 '17 at 13:18
  • Shouldn't the fundamental types be points (0D)? Now, if you change a face of the solid, its faces may no longer join up. On the other hand, if your fundamental objects are points, then if you change a point, the edges and faces and hence the solid will still join up. – Walter Aug 01 '17 at 13:44
  • @walter, ultimately, I'd like to add `Vertex` to this design, which again `Solid` has access too but primarily `Edge` manages. I wanted to keep the example simple though. – wesanyer Aug 01 '17 at 14:02
  • @AndyG I liked the idea of using `shared_ptr` etc. in `Solid`. I suppose, then, that `Solid`s constructor would have to accept a vector of `shared_ptr` and `shared_ptr`. I can then use @CoryKramer and @meowgoesthedog's suggestion to pass each `Face` and `Edge` an index - and I guess a reference to `Solid`s vectors? So, is there name for a design pattern of this style? If so I'd like to do some reading/research on it. – wesanyer Aug 01 '17 at 14:04

1 Answers1

0

There are many ways of managing these sorts of relationships but if you want efficiency and you want to share vertices between edges and share edges between faces then I suggest your Solid should own a complete list of Vertex and Edges.

Then Edge has some sort of non-owning reference to its vertices and Face has some sort of non-owning reference to its edges. Those non-owning references could be something like a pointer but you then have to be careful you don't invalidate those pointers by reallocating the main list of vertices or edges. It is safer if you store indices instead. But that does mean you have to refer to Solid to find out what a vertex/edge index refers to:

class Solid {
  std::vector<Vertex> vertices;
  std::vector<Edge> edges;
  std::vector<Face> faces;

public:
  Solid(std::vector<Vertex> vertices) : vertices(std::move(vertices)) {}

  void addEdge(int vertex_index1, int vertex_index2) {
    edges.emplace_back(vertex_index1, vertex_index2);
  }
  void addFace(std::vector<int> edge_indices) {
    faces.emplace_back(std::move(edge_indices));
  }
  const Vertex &getVertex(int vertex_index) const { return vertices[vertex_index]; }
  const Edge &getEdge(int edge_index) const { return edges[edge_index]; }
};

class Edge {
  int vertex_first;
  int vertex_second;

public:
  Edge(int vertex_first, int vertex_second)
      : vertex_first(vertex_first), vertex_second(vertex_second) {}

  const Vertex &getVertexFirst(const Solid &solid) const {
    return solid.getVertex(vertex_first);
  }
  const Vertex &getVertexSecond(const Solid &solid) const {
    return solid.getVertex(vertex_second);
  }
};

class Face {
  std::vector<int> edge_indices;

  int getEdgeIndex(int face_edge_index) const {
    return edge_indices[face_edge_index];
  }

public:
  Face(std::vector<int> edge_indices) : edge_indices(std::move(edge_indices)) {}

  const Edge &getEdge(int face_edge_index, const Solid &solid) const {
    return solid.getEdge(getEdgeIndex(face_edge_index));
  }
};

Live demo.

An alternative is to use std::shared_ptr for Edge and Vertex but then you have to pay for the dynamic memory allocation and poorer data locality.

It is tempting to store a back-reference to Solid inside Face and Edge for better encapsulation. You could do that but then the vector of Face and Edge effectively contain a lot of duplicate pointers. If that sort of encapsulation is important to you I suggest you create some sort of wrapper classes for working with edges and faces that contain the raw edge/face object and also a back-reference to the Solid.

Chris Drew
  • 14,926
  • 3
  • 34
  • 54