Extending the Standard Template Library with Association Classes

These classes extend the STL container concepts to manage complex relationships among objects.


September 01, 2001
URL:http://www.drdobbs.com/extending-the-standard-template-library/184401437

September 2001/Extending the Standard Template Library with Association Classes/Listing 1

Listing 1: Partial listing of Relations.h — association class definitions

//Relations.h
#ifndef _RELATIONS_H
#define _RELATIONS_H

#include <map>
#include <vector>
#include <algorithm>

using namespace std;

// OneOne relationship template class
template < class First, class Second, 
           class CompareFirst = less<First>,
           class CompareSecond = less<Second> >
class OneOne
{

   typedef map <First,  Second, CompareFirst> FirstSecond;
   typedef map <Second, First, CompareSecond>SecondFirst;

   FirstSecond _FirstSecond;
   SecondFirst _SecondFirst;

public:

   // Insert a pair of elements
   bool insert(const First& f, const Second& s);

   // Remove a pair with this First if it was inserted
   bool removeByFirst(const First& f);

   // Remove a pair with this Second if it was inserted
   bool removeBySecond(const Second& s);

   // Return the Second item for this First
   // or NULL if it doesn't exist
   Second * getSecond(const First& f);

   // Return the First item for this Second
   // or NULL if it doesn't exist
   First * getFirst(const Second& s);

   void clear(); // Clear this relationship

   // Define iterator for this relationship
   typedef FirstSecond::iterator iterator;

   iterator begin() { return _FirstSecond.begin(); }
   iterator end() { return _FirstSecond.end(); }
};

//--------------------------------------------------------------

// OneMany relationship template class
template <class One, class Many,
          class CompareOne = less<One>,
          class CompareMany= less<Many> >

class OneMany
{

   //maps One to several Many
   typedef multimap <One, Many, CompareOne> OneManyMap;
   OneManyMap _OneManyMap;

   //maps each Many to its One
   typedef map <Many, One, CompareMany> ManyOneMap;
   ManyOneMap _ManyOneMap;

public:
   // Insert this One Many pair to this relationship
   // if it is not already there
   bool insert(const One& o, const Many& m);

   // Remove this Many if it was inserted
   bool removeByMany(const Many& m);
        
   // Remove this One if it was inserted
   bool removeByOne(const One& o);

   // Return the One for this Many
   // or NULL if it doesn't exist
   One* getOne(const Many& m){

   // Nested iterator class for Many 
   class ManysIterator{...omitted...};

   typedef ManysIterator <One, Many> iterator;

   // Nest const iterator class for Many
   class ConstManysIterator 
      : public ManysIterator{...omitted...};

   typedef ConstManysIterator <One, Many> const_iterator;

   // The begin and end iterators for this One
   iterator begin(const One& o);
   iterator end(const One& o);

   // The const begin and end iterators for this One
   const_iterator begin(const One& o) const;
   const_iterator end(const One& o) const;

   void clear(); // Clear this relationship
};

//--------------------------------------------------------------

// triple struct used by ManyMany relationship
template <class T1, class T2, class T3>
struct triple : public pair<T1, T2>{

   triple(const T1& x, const T2& y, const T3& z) 
   : pair<T1, T2>(x, y), third(z){} 

   T3 third;
};

template <class T1, class T2, class T3>
inline triple<T1, T2, T3> 
make_triple(const T1& x, const T2& y, const T3& z){
   return triple<T1, T2, T3>(x, y, z);
}

// ValToType struct to determine type; used at compile time
template <class T1, class T2, class T3>
struct ValToType{ typedef triple <T1, T2, T3> type; };

// Partial template specialization of ValToType class
template <class T1, class T2>
struct ValToType<T1, T2, void>{ typedef pair <T1, T2> type; };

ManyMany relationship template class
template <class Many1, class Many2,
          class CompareMany1 = less<Many1>,
          class CompareMany2 = less<Many2>,
          class Rel          = void>
class ManyMany{

   typedef ValToType< Many1, Many2, Rel >::type  Relation;
   typedef vector<Relation> Relations;
   Relations _Relations;

   typedef multimap <Many1, Relation, CompareMany1> One1Many2Map;
   One1Many2Map _One1Many2Map;

   typedef multimap <Many2, Relation, CompareMany2> One2Many1Map;
   One2Many1Map _One2Many1Map;

public:

   // Insert this Many1 Many2 pair to the relationship
   bool insert(const Many1& m1, const Many2& m2){

      Relation rel(m1, m2);
      _Relations.push_back(rel);

      _One1Many2Map.insert( make_pair (m1, rel) );
      _One2Many1Map.insert( make_pair (m2, rel) );

      return true;
   }

   // Insert this Many1 Many2 Rel triple to the relationship
   bool insert(const Many1& m1, const Many2& m2, const Rel& r);

   // Remove this Many1
   bool removeByMany1(const Many1& m1);

   // Remove this Many2
   bool removeByMany2(const Many2& m2);

   Nested functor struct used to find relations
   struct eqRel{...omitted...};

   // Remove this Many1 Many2 pair
   bool remove(const Many1& m1, const Many2& m2);
  
   // Return the Link Attribute for this pair of Many1 Many2
   // or NULL if not found
   const Rel* getAttribute(const Many1& m1, const Many2& m2);

   //--------------------------------------------------
   // Nested iterator class for iterating over Many1s
   class Manys1Iterator{

   protected:
      typedef One2Many1Map::iterator OMI;
      OMI _mi;

      typedef bidirectional_iterator_tag iterator_category;
      typedef Manys1Iterator<Many1, Many2> self;

   public:

      Manys1Iterator(OMI mi = OMI()) : _mi(mi){}
      ...
      self& operator++() {
         _mi++;
         return *this;
      }
      ...
   };
   //--------------------------------------------------

   class Manys2Iterator{...omitted...};

   // Iterator types to iterate through Many1's and Many2's
   // respectively
   typedef Manys1Iterator <Many1, Many2> iterator1;
   typedef Manys2Iterator <Many1, Many2> iterator2;

   // The begin and end iterators for this Many2
   iterator1 begin(const Many2 & m2)
   { return iterator1(_One2Many1Map.lower_bound(m2)); }
   iterator1 end(const Many2 & m2)  
   { return iterator1(_One2Many1Map.upper_bound(m2)); }

   // The begin and end iterators for this Many1
   iterator2 begin(const Many1 & m1)
   { return iterator2(_One1Many2Map.lower_bound(m1)); }
   iterator2 end(const Many1 & m1)  
   { return iterator2(_One1Many2Map.upper_bound(m1)); }

   void clear(){ // Clear this relationship

      _One1Many2Map.clear();
      _One2Many1Map.clear();
      _Relations.clear();
   }
};


#endif
— End of Listing —
September 2001/Extending the Standard Template Library with Association Classes/Listing 2

Listing 2: Example of using OneMany class

#include "Relations.h"
#include <string>

using namespace std;

//function prototypes
void listChildren(const OneMany<string, string>& MotherChildren, 
                  const string& Mother);
void insertChild(OneMany<string, string>& MotherChildren, 
                 const string& Mother, const string& Child);

void main(){

   // declare the relationship
   // each mother might have many children but each child indeed 
   // has only one mother
   OneMany<string, string> MotherChildren;

   string firstMother("Jane");
   string secondMother("Ann");
   string child;

   child = "Paul";
   insertChild(MotherChildren, firstMother, child);

   child = "Paul";
   // will fail because Paul is already in the relationship
   insertChild(MotherChildren, firstMother, child);

   child = "Richard";
   insertChild(MotherChildren, firstMother, child);

   child = "Ann";
   insertChild(MotherChildren, firstMother, child);

   child = "Alex";
   insertChild(MotherChildren, firstMother, child);

   /////////////////////////////////////////////

   child = "Cathy";
   insertChild(MotherChildren, secondMother, child);

   //will fail because Richard is Jane's son
   child = "Richard";
   insertChild(MotherChildren, secondMother, child);

   child = "Peter";
   insertChild(MotherChildren, secondMother, child);

   child = "Mary";
   insertChild(MotherChildren, secondMother, child);

   cout << "Listing Jane's children:" << endl;
   listChildren(MotherChildren, firstMother);
   cout << endl;

   cout << "Listing Ann's children:" << endl;
   listChildren(MotherChildren, secondMother);

   cout << endl;

   string* pMother = MotherChildren.getOne("Ann");
   if(NULL != pMother)
    cout << "Ann's mother is " << *pMother << endl;

}

void listChildren(const OneMany<string, string>& MotherChildren, 
   const string& Mother){

   //Iterate through all children
   for(OneMany<string, string>::const_iterator it = 
          MotherChildren.begin(Mother), 
          end = MotherChildren.end(Mother); it != end; ++it)
      cout << *it << endl;
}

void insertChild(OneMany<string, string>& MotherChildren, 
                 const string& Mother, const string& Child){

if(!MotherChildren.insert(Mother, Child))
   cout << ">>>Can't insert " << Child << " into relationship " 
        << Mother << "-" << Child <<  endl;
}

/*
Output:

>>>Can't insert Paul into relationship Jane-Paul
>>>Can't insert Richard into relationship Ann-Richard
Listing Jane's children:
Paul
Richard
Ann
Alex

Listing Ann's children:
Cathy
Peter
Mary
Ann's mother is Jane

*/
— End of Listing —
September 2001/Extending the Standard Template Library with Association Classes/Listing 3

Listing 3: Example of using ManyMany class with and without link attribute

#include <string>
#include "Relations.h"
#include <iostream>
using namespace std;

//Link attribute structure
//Uniquely identifies a student-class link
struct Record{

   Record(const string& grade = string(), int absenses = 0) 
      : Grade(grade), Absenses(absenses){}

   string Grade;
   int    Absenses;
};

main(){

   /*
   There is a many-to-many relationship between students and 
   classes: a student might be in many classes and a class might 
   have many students
   */

    //Many-To-Many relationship with a link attribute
    typedef ManyMany <string, long, less<string>, less<long>, 
                      Record> School_Rel;
    School_Rel school;

    Record r1("A", 1);
    school.insert("Peter", 101, r1);

    Record r2("B", 2);
    school.insert("Peter", 102, r2);

    Record r3("C", 3);
    school.insert("Peter", 103, r3);

    Record r4("A", 1);
    school.insert("Jane", 101, r4);

    Record r5("B", 1);
    school.insert("Jane", 103, r5);

    Record r6("A", 4);
    school.insert("Jane", 104, r6);

    cout << "Listing Peter's classes:" << endl;
    for(School_Rel::iterator2 it = school.begin("Peter"), 
        end = school.end("Peter"); it != end; ++it)
        cout << *it << endl;

    cout << endl;

    cout << "Listing students in 103: " << endl;
    for(School_Rel::iterator1 it = school.begin(103),
        end = school.end(103); it != end; ++it)
       cout << *it << endl;

    cout << endl;

    const Record* r = school.getAttribute("Peter", 103);

    if(r){
        cout << "Peter's  Grade and absenses for 103:" << endl;
        cout << "Grade - " << r->Grade << " Absenses - " 
             << r->Absenses << endl;
    }

    ////////////////////////////////////////////////////////////

    ////Many-To-Many relationship without a link attribute
    ManyMany <string, long> school1;

    school1.insert("Peter", 101);
    school1.insert("Peter", 102);
    school1.insert("Peter", 103);
    school1.insert("Jane", 101);
    school1.insert("Jane", 103);
    school1.insert("Jane", 104);

    cout << "Listing Jane's classes:" << endl;
    for(ManyMany <string, long> ::iterator2 it = 
        school1.begin("Jane"), end = school1.end("Jane");
        it != end; ++it)
        cout << *it << endl;

    cout << endl;

    cout << "Listing students in 101: " << endl;
    for(ManyMany <string, long>::iterator1 it = 
        school1.begin(101), end = school1.end(101);
        it != end; ++it)
        cout << *it << endl;
}

/*
Output:

Listing Peter's classes:
101
102
103

Listing students in 103:
Peter
Jane

Peter's  Grade and absenses for 103:
Grade - C Absenses - 3
Listing Jane's classes:
101
103
104

Listing students in 101:
Peter
Jane

*/
— End of Listing —
September 2001/Extending the Standard Template Library with Association Classes/Sidebar

Field Experience


The relationship classes described in this article were originally written for and used in the construction of an interprocedural data-flow analyzer for programs written in Ada. Data-flow analysis is used for various compiler optimizations and automatic generation of test cases. As is true for the majority of compilation-related tools, data-flow analyzers are extremely data intensive, making use of many data structures such as symbol tables, type tables, data-flow graphs, and others. These data structures must be efficiently representable in memory, providing quick access to the required information. In order to manage the complexity of the task, it makes sense to organize the analyzer as a set of logical software modules. These modules have distinct functionality and many of them need access to a common set of shared data structures.

For example, the symbol table structure is constructed by the parser module and then used by many other modules. The symbol table must keep track of local definitions and uses of variables. The table of local definition uses is constructed by walking the parse tree; it is subsequently used in several other phases, such as constructing data-flow equations. To make such data easily accessible, we made all data structures global. Placing each data structure into its own namespace helped avoid name clashes:

namespace LocDefUse{
ManyMany<Def*, Line> _defs;
ManyMany<Use*, Line> _uses;
...
};

namespace SymbolTable {
...
}

As a result of using the described association classes, many complex algorithms in our analyzer became easier to implement. In data-flow analysis, a definition of a variable is an action that changes the value of the variable’s memory location. An assignment is the most common form of a definition. Thus, definitions and line numbers form a many-to-many association: there can be many definitions on a line and the same variable may be defined on many lines.

The kill set of a variable definition consists of all other assignments to the variable in the same function. (It’s called the “kill set” because a definition “kills” all other definitions, including itself if it’s part of a loop.)

The following snippet shows how an association class can be used to construct the kill set for each definition on a given line:

ManyMany<Def*, Line>::iterator1
   it1 = LocDefUse::_defs.begin(line),
   end1 = LocDefUse::_defs.end(line);
//for all definition on this line
for(; it1 != end1; ++it1){
   Def* pDef = *it1;
   ManyMany<Def*, Line>::iterator2
      it1 = LocDefUse::_defs.begin(pDef),
      end2 = LocDefUse::_defs.end(pDef);

   //for all definitions of this variable in the function
   for(; it2 != end2; ++it2){
      Line l = *it2;
            ...
   }
}

The second for-loop goes through all the lines that also define this definition’s variable. The pseudocode for the example might look like this:

For all definitions on this line
   For each definition of a variable,
      go through all the lines where this
      variable is defined.

Since a definition kills all other definitions, the second for-loop produces the kill set. Thus the use of ManyMany association here significantly simplifies the kill set computation.

September 2001/Extending the Standard Template Library with Association Classes

Extending the Standard Template Library with Association Classes

Eli Tilevich

These classes extend the STL container concepts to manage complex relationships among objects.


Introduction

The STL (Standard Template Library) provides a large set of standard data structures called containers and many standard algorithms to operate on them. Using this library allows programmers to concentrate on the higher-level design without worrying about many implementation details when it comes to data structures and algorithms. Despite the abundance of different containers in the STL, representing associations between two application classes still can be quite challenging. Such an association is actually a relationship that exists between different kinds of objects. You may, for example, wish to create a data structure that represents all the employees who belong to a particular department, or that represents all the interconnections between nodes in a network. In this article I explain why such associations are difficult to represent using just the STL, and I describe a set of classes that enable them to be represented naturally. Of particular importance, these classes effectively decouple the objects that participate in such relationships.

The Problem of Representing Associations

Representing associations with STL containers is difficult because none of the containers, with the exception of std::map and std::multimap, are Pair Associative [1]. A Pair Associative container stores elements that consist of pairs of sub-elements; the other STL containers store elements of just one particular type (or of base-class pointers to polymorphic types).

Storing entities in containers that are not Pair Associative just groups them together, it does not represent any associations between two different classes of objects. For example, an STL vector of pointers to Employee objects provides little information about how objects of the Employee class relate to other kinds of objects in the program. In contrast, a Pair Associative container, such as a std::map<Employee *, Department *>, accomplishes two goals: it stores Employee and Department pointers, and shows that each Employee has an associated department.

The map and multimap classes, which are the only Pair Associative containers in the STL, quite often are not sufficient to represent all possible associations between application classes. There are three types of possible associations between objects of two classes: one-to-one, one-to-many, and many-to-many. One-to-one means that an object of the first type relates to one and only one object of the second type. For instance, for each Employee object there is one and only one unique ID object. One-to-many means that for each object of the first type there can be many objects of the second type but for each object of the second type there is one and only one object of the first type. For example, there can be many Employees in one Department but each Employee belongs to one and only one Department. And finally, many-to-many means that for each object of the first type there can be many objects of the second type and vice versa. For example, an Employee can be enrolled in many Courses and there can be many Employees enrolled in a particular Course.

An Unsatisfactory Solution

When it comes to representing such associations with the STL, there are several choices. Consider a common textbook problem in which there are a set of employees and a set of departments. I will show how this problem can be represented in a C++ program.

One approach is to use a non-Pair-Associative container inside an application class. Consider the following class declaration:

class Employee;
class Department{
   set <Employee *> _employees.
   typedef set 
    <Employee *>::iterator empl_iterator;

public:
   ...
   void addEmployee(const Employee* e);
   bool deleteEmployee
    (const Employee* e);
   empl_iterator beginEmployee();
   empl_iterator endEmployee();
};

This declaration implies a possible one-to-many association between classes Department and Employee. A close examination reveals several shortcomings of this approach. Suppose you need to access the set of Employee pointers (_employees) from outside the Department class. If this data member is made private, more methods will be required to access it. Besides, it is not clear what access methods should be provided. Providing a method that returns a reference to the container defeats the data encapsulation principle — it might as well be declared public. Instead, the class designer would probably want to provide methods for adding and deleting items from the container and searching and traversing the container itself. It becomes rather problematic to enforce a uniform coding convention in providing such methods.

Understanding this code is problematic as well. If this declaration is the only piece of information that exists, there is no way to tell whether it represents just a one-to-many association. For example, if some of the employees belong to multiple departments, we are dealing with a many-to-many association. In many cases, proper maintenance of this code is impossible without understanding what type of association may exist. The only way to find out what type of association may exist is to carefully study not just the implementation of the Employee and Department classes but also all their interactions with other classes. Finally, since all the data relationships are hidden inside each individual class declaration, writing code to iterate through the application’s data becomes possible only if we have previously studied each application class’s declaration.

Using Association Classes

A different approach is to use special-purpose association classes. Application classes no longer have to keep track of which associations they participate in. All associations are externalized and put in one place:

namespace DataOrg{

//each department has a unique ID
OneOne<Department*, int> Dept2ID;
typedef OneOne<Department*, 
  int>::iterator DeptIter;

//There are can be many employees in one
//department but each employee belongs 
//to only one department
OneMany<Department*, 
  Employee*> Employees;
typedef OneMany<Department*, 
  Employee*>::iterator EmplIterator;

//Each employee can participate in 
//several projects and each project can 
//have many employees
ManyMany<Employee*, Project*> Projects;
typedef ManyMany<Employee*, 
  Project*>::iterator1 EmplByProjIter;
typedef ManyMany<Employee*, 
  Project*>::iterator2 ProjByEmplIter;

}; //end DataOrg namespace

Partial definitions of the template classes OneOne, OneMany, and ManyMany are shown in Listing 1. This listing also shows the implementation of a few of the methods in the ManyMany class, including insert, begin, end, and operator++ of one of the nested iterator classes. These template classes are like STL containers in a couple of ways: they can store more than one element, and they can grow or shrink dynamically. Thus, the OneOne container can store any number of one-to-one associations. But the association classes are not just ordinary containers; for one thing, they provide more kinds of iterators. For example, given the many-to-many Projects container as defined above, it is possible to obtain an iterator that iterates over all the Employees involved in a particular Project; but it is also possible to iterate over all the Projects being worked on by a particular Employee.

Using these classes, suddenly all the data associations in the program become very clear, which makes the program easier to manage and understand. For example, if the requirements should change and employees can no longer share projects, the programmer can just change the type of the Projects association from ManyMany to OneMany. The data integrity requirement (making sure that there are no two employees sharing the same project) will be enforced by the OneMany class itself and there is no need to provide any additional code for that purpose. Since all class associations are now externalized, it is easy to add a new association to the program.

Consider how you would add logic to organize Projects by Departments using both the old and new approaches. Without the association classes, you would have to add a vector of Projects to the Department class. In addition to this, you would also have to add several methods to access projects, add them to the Department, and delete them. Using the new approach, you need only add another association:

OneMany<Department*, Project*> ProjsByDept;

The OneMany class already has all the methods for adding, deleting, and iterating projects for each department inserted into it. An interesting question arises: where should objects of such association classes be created? We could either make them to be members of the main application class or declare them global, depending on other design factors.

Design and Implementation Considerations

Later in the article I provide a few examples that show how standard general-purpose association classes can be useful. Regardless of how they are implemented, there are several design considerations that should be taken into account. One decision is whether to make those classes intrusive or not. By intrusive I mean that the pointers that are used to maintain the associations are stored inside application classes. There are several existing class libraries that take this approach. The DataObject Library from CodeFarms Inc. uses a code generator to put such pointers inside application classes. The Pattern Template Library from the same company uses template-based inheritance to do the same thing. The latter class library also provides a many-to-many data structure based on the intrusive approach.

The obvious advantage of using the intrusive approach is performance. For example, if each many in a OneMany association stores a pointer to its one, then the one associated with a particular many can be retrieved in constant time. Where no such back-pointer exists, finding the one associated with a particular many typically requires a search operation. The only drawback of the intrusive approach is that if the data organization of the program changes, the programmer must either rerun a code generator, or change the inheritance structure of the application classes.

Since the STL has become a de-facto industry standard (not to mention a part of the C++ Standard), I designed my relationship classes to extend the STL, and I wanted to make them as STL compatible as possible. My goal was that anyone familiar with the STL should be able to start using the three relationship classes right away. Since all the STL containers are non-intrusive, I have chosen a non-intrusive approach as well. I use the STL map and multimap classes to implement the internal structure of the relationship classes. map and multimap utilize a balanced search tree (specifically, a “red-black” tree), so all insertion, removal, and retrieval operations have logarithmic complexity. This is of course less efficient than the constant access complexity of intrusive data structures; however, for reasonably-sized data the difference is not significant.

The three association classes, OneOne, OneMany, and ManyMany, were designed to be extremely flexible to fit the general programming purpose. Each class provides a standard interface to insert and remove the data, as well as STL-style iterators to traverse through the stored data. The ManyMany class provides two types of iterators for each application class participating in the association.

I had to take particular care to make the ManyMany association flexible enough to be useful in different programming scenarios. The question of how to deal with many-to-many associations has been tackled by many OO designers. Rumbaugh et. al suggest that

the best approach is usually to implement the association as a distinct class, in which each instance represents one link and its attributes

This quotation introduces the notion of link attributes. A link attribute is a piece of data that applies only to the association between two entities, not to either entity in itself. For instance, in the case of a many-to-many relationship between Employee and Project, possible link attributes would be the hours put in, the start and end date, and some other information that applies only to a particular employee working on a particular project. A class to hold such data might be called EmplProj and the class would serve as a link attribute.

A problem arises because sometimes a link attribute is not needed. This implies the necessity of having two different internal data organizations for the ManyMany class. That’s where the advanced C++ feature called partial template specialization comes into play. The ManyMany class can be specialized in two different ways. If the link attribute is not used, the class should be specialized with the two application classes participating in the association, and possibly with the corresponding less-than predicates for each application class. (If the predicates are not specified, the default STL less predicate will be used.) If the link attribute is needed, then the specialization requires five parameters: the first two are the application classes followed by their corresponding less-than predicates, with the link attribute class at the end. In the ManyMany class implementation, the fifth template parameter is defaulted to type void. This generates the correct internal representation and the partial template specialization does the magic.

The ManyMany class provides two overloaded insert methods. The first two parameters corresponding to the elements stored in the associations are identical in both methods. The third parameter in the second insert method is the link attribute. Choosing an incorrect insert method that is not supported by the chosen specialization will result in a compiler error. The same holds true for the getAttribute method that returns a link attribute given two entities participating in the relationship. If the ManyMany class was not instantiated with a link attribute, the compiler is not be able to generate code for the getAttribute method and signals an error.

Unfortunately, not all C++ compilers support the partial template specialization feature. I have tested the sample code using egcs-1.1.2 running on both Solaris and Linux.

Examples

The following examples show a couple of simple uses for association classes. To get an idea how useful they are in practice, see the sidebar, “Field Experience.”

To use these association classes, you need only include the file Relations.h, partially shown in Listing 1. (The full source code is available on the CUJ website at <www.cuj.com/code>.)

Listing 2 (MotherChildren.C) demonstrates a typical use of the one-to-many association. This example if often referred to in database literature as an “obvious” case of such use: while a mother can have one or more children, each child has only one mother. The listing shows the definition of two functions besides main: insertChild and listChildren. The first one shows how the OneMany class enforces the integrity of the association and the second demonstrates a use of the class’s iterators.

This example demonstrate that the iterators provided for the association classes behave a little differently than STL iterators. Consider the following statement from the listChildren function defined in Listing 2:

OneMany<string, string>::const_iterator it =
   MotherChildren.begin(Mother),
   end = MotherChildren.end(Mother);

Unlike the begin and end functions in STL containers, these begin and end functions take a parameter. Here, begin returns an iterator to all children who have the mother represented by the string Mother. The end function returns an iterator that points one-past-the-end of this range of children.

Listing 3 (StudentClasses.C) shows a textbook example of the many-to-many association. Indeed a student can take many classes and there can be many students in one class. This listing shows how the ManyMany classes can be instantiated with and without the link attribute template parameter. In this example, the link attribute of the association is expressed via the means of the Record struct. This struct contains information that applies to a unique student-class association: the number of absences and the student’s grade for a given class. The listing also shows a typical use of the class’s iterators.

Conclusion

Using the special purpose association classes makes the resulting code easier to manage and understand. These classes can easily be ported to Java, which starting from JDK 1.2, supports the Collections framework with many useful Pair Associative data structures such as Map and HashMap. In addition, I would like to propose that the C++ standards committee consider adding relationship classes to the STL. I would not insist either on this particular implementation or even on the method interface that I have provided. However, I would like to make a point that such classes are generic enough and difficult enough to implement to be considered a useful addition to the STL.

Note

[1] The term Pair Associative comes from Matt Austern’s book, Generic Programming and the STL (Addison-Wesley, 1999).

Further Reading

Jiri Soukup. Taming C++: Pattern Classes and Persistence for Large Projects (Addison-Wesley, 1994).

Jiri Soukup. “Writing Complex Software,” Dr. Dobb’s Journal, October 1999.

Alexander Stepanov and Mung Lee. "The Standard Template Library," (Hewlett-Packard), 1501 Page Mill Rd., Palo Alto, CA 94304, Aug.18/94

James Rumbaugh. Object-Oriented Modeling and Design (Prentice Hall, 1991).

Bjarne Stroustrup. The C++ Programming Language (Addison-Wesley, 1997).

Eli Tilevich is a Ph.D. student and an IT consultant. He has been using C++ since the early 90’s for many commercial and research large-scale projects. His interests include programming languages, compilers, and distributed computing. His current research deals with the issues of automatic partitioning and distribution of Java programs. He can be contacted at [email protected].

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.