Next Previous Contents

3.5 Adding and using node/edge attributes

Semantic attributes are handled by the Graph class through void * pointers, and so any built-in or user defined type can be used for node or edge attributes.

The use of pointers has the advantage of making possible to manage the attributes efficiently, avoiding unnecessary copies, and allowing different graphs to share the memory areas used for the attributes. On the other hand, especially in a language like C++ that does not support automatic garbage collection, there is the problem of the ownership of the pointed data structure: who is responsible for allocating the attribute, for cloning it (i.e. allocating a new copy of it) when necessary, and for deallocating it?

The easiest answer for the user would be: the Graph class. But this choice would have implied excessive limitations to the flexibility of the attribute management.

For example, in many situations it would be reasonable to have the attributes dynamically allocated on the heap, with a separated attribute instance for each node/edge. However there are cases where only a very limited set of attribute values are possible, and so a great memory saving can be obtained by preallocating these values (possibly with static allocation) and sharing the pointers among all the nodes/edges of the graphs. Giving the Graph class the responsibility for the allocation/deallocation of the attributes would imply that only one allocation policy must be chosen, at the price of an efficiency loss in the cases where this policy is not the best one.

So, the rules we have followed in the design of the library are:

In order to avoid the need of pointer casting when dealing with attributes, the library provides a template class ARGraph<N,E>, derived from Graph, for graphs with node attributes of type N and edge attributes of type E.

How are attributes employed in the matching process? The user can provide an object of the graph class with a node comparator and an edge comparator. These are objects implementing the AttrComparator abstract class, which has a compatible method taking two attribute pointers, and returning a bool value that is false if the corresponding nodes or edges are to be considered incompatible and must not be paired in the matching process. In this way, the search space can be profitably pruned removing semantically undesirable matchings. Notice that the matching algorithm uses the comparators of the first of the two graphs used to construct the initial state.

Now let us turn to a practical example. Suppose that our nodes must represent points in a plane; we will associate with each node an attribute holding its cartesian coordinates. For simplicity, the edges will have no attributes. Suppose we have a class Point to represent the node attributes:



class Point
  { public:
      float x, y;
      Point(float x, float y)
        { this->x=x;
          this->y=y;
        }
  };

Now, if we want to allocate the attributes on heap, we need a destroyer class, which is an implementation of the abstract class AttrDestroyer. In our example, the destroyer could be:


class PointDestroyer: public AttrDestroyer
  { public:
       virtual void destroy(void *p)
         { delete p;
         }
  };

We will also need a comparator class for testing two points for compatibility during the matching process. A comparator is an implementation of the abstract class AttrComparator. Suppose that we consider two points to be compatible if their euclidean distance is less than a threshold:



class PointComparator: public AttrComparator
{ private:
    double threshold;

public:
  PointComparator(double thr)
    { threshold=thr;
    }
  virtual bool compatible(void *pa, void *pb)
    { Point *a = (Point *)pa;
      Point *b = (Point *)pb;
      double dist = hypot(a->x - b->x, a->y - b->y);
           // Function hypot is declared in <math.h>
  
      return dist < threshold;
    }
};

We can build two graphs with these attributes using the ARGEdit class:


int main()
  { ARGEdit ed1, ed2;

    ed1.InsertNode( new Point(10.0, 7.5) );
    ed1.InsertNode( new Point(2.7, -1.9) );
    ed1.InsertEdge(1, 0, NULL);
    // ... and so on ...

    ARGraph<Point, void> g1(&ed1);
    ARGraph<Point, void> g2(&ed2);


    // Install the attribute destroyers
    g1.SetNodeDestroyer(new PointDestroyer());
    g2.SetNodeDestroyer(new PointDestroyer());

    // Install the attribute comparator
    // This needs to be done only on graph g1.
    double my_threshold=0.1; 
    g1.SetNodeComparator(new PointComparator(my_threshold));

    VFSubState s0(&g1, &g2);

    // Now matching can begin...

Notice that the attribute destroyers and comparators have to be allocated on heap with new; once they are installed they are owned by the graph, which will delete them when they are no longer needed. So it is an error to share a destroyer or a comparator across graphs, as is to use a static or automatic variable for this purpose.

Historical note: Previous versions of the library (before 2.0.5) used simple functions instead of full objects for deallocating or comparing attributes. These functions were installed using the SetNodeDestroy, SetEdgeDestroy, SetNodeCompat and SetEdgeCompat methods. While these methods are still supported for backward compatibility, we warmly recommend to use the new object-oriented approach, which provides greater flexibility. For example, with the old approach it would have been quite difficult to obtain something equivalent to a point comparator; the threshold would have had to be either a compile-time costant or a global variable, with obvious drawbacks.


Next Previous Contents