c++algorithmmatrixclass-designcovariant

C++ class design: Covariance


The Problem

I want to implement a number of algorithms that work on a graph and return scores for node-pairs indicating whether those nodes are similar. The algorithms should work on a single node-pair and on all possible node-pairs. In the latter case a collection/matrix should be returned.

My Approach

The algorithms derive from

class SimilarityAlgorithm {
public:
  Base(const Graph& G);

  virtual double run(node u, node v) = 0; // indices for nodes in the graph

  virtual ScoreCollection& runAll() = 0;
}

Now the algorithms differ in memory usage. Some algorithms might be symmetric and the scores for (u, v) and (v, u) are identical. This requires different ScoreCollection-types that should be returned. An example would be a sparse-matrix and a triangular matrix that both derive from ScoreCollection.

This would boil down to covariant return types:

class SpecificAlgorithm : SimilarityAlgorithm {
public:
  double run(node u, node v);

  // The specific algorithm is symmetric and thus uses a symmetric matrix to save memory
  SymmetricScoreCollection& runAll();
}

Question


Solution

  • Your design seems appropriate for the problem you describe.

    Problem:

    However, there is a problem with your SpecificAlgorithm : runAll() doesn't return the same type as the virtual function of the base class. So it won't be called (or more probably, your code won't compile because of a missing virtual function).

    Solution:

    Use also a polymorphic approach for the ScoreCollection, by making SymmetricScoreCollection a derived class of ScoreCollection :

    class SymetricScoreCollection: public ScoreCollection {
    //define the member functions to access the values virtual
    ...
    }; 
    
    class SpecificAlgorithm : public SimilarityAlgorithm {
    public:
      double run(node u, node v);
    
      // The specific algorithm is symmetric and thus uses a symmetric matrix to save memory
      ScoreCollection& runAll();
    };    
    

    In fact it's an application of the factory method pattern, with the following roles:

    Additional remark:

    Returning a reference to ScoreCollection from runAll() brings in some risks. Suppose sa is a specific algorithm.

    In the following statement :

    ScoreCollection sc = sa.runAll();
    

    sa.runAll() returns a reference to a SymetricScoreCollection, but it would copy the referred object to sc, making it a ScoreCollection. Slicing occurs, and polymorphism will fail to work.

    The following statement would however succeed:

    ScoreCollection& rsc = sa.runAll();
    

    because rsc is a reference and it would still refer to the original SymetricScoreCollection object returned by sa.runAll(), and everything would work as designed.

    You see that it's very easy to have unnoticed mistakes when returning references. I'd suggest to return a pointer instead of a reference.