Boost logo

Boost :

From: Dietmar Kuehl (dietmar_kuehl_at_[hidden])
Date: 1999-12-10 13:55:02


Hi,
> I've uploaded the description of the graph structure interface
> to the boost vault.

Just a brief note on the format: Although I like the format of the
description you have choosen (after all, it resembles the format I
have choosen :-) I want to point out that this is not some kind of a
must! If it looks useful to you, I'm just fine. However, I would also
change the format of the documentation if there are good suggestions,
good guidelines, or common preferences.

In any case, I think that the use of HTML is a very reasonable file
format which can be conveniently viewed by everybody (ie. the "format"
mentioned above refers only to the contents, not to the choosen markup
language).

> http://www.egroups.com/docvault/boost/graphs/graph_structure.html

- In the mathematical definition of the graph I dislike a particular
  implied restriction: Sets, by definition, do not include multiple
  copies of the same elements. That is, the definition of a graph
  you have given rules out parallel edges. This is rather typical
  for theoretical work but basically useless in practical situations:
  Parallel edges, as well as loops (ie. edges where the source and
  target vertex are identical), are present in graphs encountered in
  practise. The abstraction and the algorithms should cope with them,
  especially as most algorithms have no problem at all with these edges
  (except that the asympotical complexity looks different which is,
  however, irrelevant in practise once you have choosen an algorithm).

- Although they are low, there are requirements imposed on the
  descriptor types: These types have to be Assignable (this reminds
  me that the value type for the property accessors also has to be
  assignable for all property accessors except
ReadablePropertyAccessor;
  this is not yet mentioned...) and potentially even EqualiyComparable.

- We need to define exactly what an adjacency iterator iterates over.
  There are two options (I can see...):
  - The *set* of adjacent edges. In this case there would be
significant
    difference to incidence iterators, eg. because the cardinality of
    the resulting nodes differs if parallel edges are present. However,
    this is in general non-trivial to achieve and I can't think of an
    algorithm which really needs this feature.
  - Adjacency iterators are basically the same as incidence iterators
    except that they directly access the corresponding node. That is,
    an adjacency iterator 'ait' can be implemented in terms of an
    incidence iterator 'iit', ie. basically 'operator*()' for 'ait'
does
    this: 'return target(*iit, g)'. Of course, for specific graphs
    there can be better implementations (eg. an implementation which
    does not require a reference to the graph object).
  
  If we decide that we don't want to decide this yet because we first
  want to gain experience with the interface, there are two options:
  - Either we remove all mention for adjacency iterators for now and
    introduce it when there is experience and a need/use for this
    iterator.
  - We document clearly that the exact semantics are not yet defined
    for this kind of iterators. In this case, algorithms should
    assume that it does the wrong thing, ie. if they depend on either
    of the specific semantics, they should *not* use it.
  
- A minor nit making no semantic difference: Since the incidence
  iterator is likely to iterate over the outgoing edges for a node,
  it might be counterintuitive to call it "InIt" although I can see
  the logic of the name :-)

- The valid expression should include the following operations:

    expression type semantic
    *ait V obtain a vertex descriptor from an
                              adjacency iterator
    *iit E obtain an edge descriptor from an
                              incidence iterator
    *vit V obtain a vertex descriptor from a
                              vertex iterator
    *eit E obtain an edge descriptor from an
                              edge iterator

  I was also thinking about an orthogonal use of a pair for the
incident
  nodes of an edge, ie.

    incident(e,g) pair<V,V> get descriptors for the nodes incident
                              to an edge.

  instead of the functions source() and target(). I have no strong
  opinion on this one, though (actually, there are arguments against
  this, namely separation of requirements; see below: I thought of
  this only much after first writing the suggestion...).

- What is the "head" or "source" vertex of a graph? Where is it
  used?

- Instead of guaranteeing the complexity I would "assume" it! This
  way it would be possible to apply the abstraction even to graphs
  having a different time complexity. However, in this case the
  complexity given for the algorithm does not necessarily hold.

- In addition to requiring the possibility of multiple passes, I
  think the following requirement also has to be added:
    it1 == it2 => *it1 == *it2
  This definition, of course, implies that descriptors support the
  comparison. This could be a reasonable requirement by itself (and
  should in this case be listed with the descriptors) but even if
  this is not required, a similar requirement providing object
  identify should be formulated.

A major issue is how to document and determine at compile time
requirements for graphs. My preference would go a different approach
especially with respect to orthogonal requirements: There are
application
where the list of all edges are required, applications where the list
of
all nodes are required, as well as applications where neither or both
are required. A graph supporting these featues just lists them in its
traits, a graph not supporting them does nothing, in particular does
not provide typedefs for the corresponding types: If they are accessed
by an algorithm it is an error anyway.

It can be argued that at compile time it could be useful to have the
typedef for 'void' to specialize for this type (actually, I'm not sure
whether this is indeed possible but using some other type invented
specifically for this purpose would definitely do). Thus, this way the
compile time detection of features can be achieved. This is definitely
true but this approach is not extensible to future properties like
eg. support for special graph categories like planar or directed
acyclic
graphs.

I would prefer an approach where the graph traits can be extended as
needed without having to change existing graph traits (except if the
corresponding graphs support more specific traits which are not yet
listed; ie. the traits for graphs which have nothing to do with the new
traits should go unchanged). A simple approach are the special classes
I outlined in a previous mail. The same approach which can be used to
determine whether a graph structure guarantees planar graphs can be
used to determine whether a graph structure supports iteratation over
all nodes. The details may look different, the general outline could
look something like this:

  struct is_supported { static const bool flag = true; };
  struct is_not_supported { static const bool flag = false; };

  template <typename Property, typename Graph>
  struct graph_property { typedef is_not_supported support; };

  struct planar_graph {};
  struct directed_acyclic_graph {};
  // ...

If a graph class G supports some property, it would just specialize
the class 'graph_property', implement the corresponding access
functions, and provide corresponding typedefs in the graph traits
class. To test for a specific property, specialization can be used
on the subtype 'support' or the static member 'flag' of this type
can be used in conditionals. For example:

  // note that alway the default for the template argument is used!
  template <typename G,
    typename dummy = typename graph_property<planar_graph, G>::support>
  class algo1
  {
    // ...
  };
  template <typename G>
  class algo1<G, is_supported>
  {
    // ...
  };

  template <typename G>
  class algo2
  {
    void foo() {
      if (graph_property<planar_graph, G>::support::flag)
        something_assuming_planarity_but_no_special_functions();
      else
        handling_the_general_case();
    }
    // ...
  };

There are two things to note about this code:
- The default template argument for the first style of specialization
  is always used, ie. this template argument is never explicitly
  provided! This template argument is only there for specialization.
- The second style only works if the special case does not require
  any special interface (unless the called function is specialized;
  however, I have more in mind that no function is actually called
  anyway). However, sometimes certain extra operations can be omitted
if
  a certain structure is known for the graph. For example, in a
directed
  acyclic graph there are no "backward edges" closing a cycle during a
  graph search. This does not need a special interface, just the
knowledge
  that it is not there.

The handling of a list of nodes, edge, adjacent nodes, etc. can be
done similarily. Only some basic operations should go into the
general graph traits. Thinking of it, this is of course a reason
to separate the source() and target() functions for an edge: There
are graph representations supporting only one of them (the target()
operation) but not the other... Thus, the pair idea I mentioned above
is not really that clever (I left it in anyway so others don't have to
go into this dead end).

Regards,
  dk
__________________________________________________
Do You Yahoo!?
Thousands of Stores. Millions of Products. All in one place.
Yahoo! Shopping: http://shopping.yahoo.com


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk