CSE 428: Lecture Notes 14


The sieve of Eratosthenes in Object-Oriented style

The sieve of Eratosthenes is an algorithm which generates the sequence of prime numbers starting from 2. The idea consists in generating the sequence of natural number starting from 2. For each new natural number n, we check whether n is a multiple of any of the prime numbers generated so far. If not, then n is a prime number.

This algorithm is particularly suitable to be the basis for a nice example of Object Oriented Programming. We will see how to program it in C++. For more details, see also the book of Sethi, Section 7.5.

The general idea is to create dynamically new objects, one for each prime number which is generated, and to let each of these object be responsible for eliminating (filtering away) numbers which are not prime. The simplest organization is to have all these objects connected in a chain, and new numbers (generated by an object at the beginning of the chain) flowing through the chain. The picture below illustrates the organization of the objects and the flow of data:

       -------------
       |  Counter  |   generates natural numbers
       -------------
            ||         | flow of data
            \/         V
       -------------
       | Filter 2  |   eliminates numbers that are multiples of 2
       -------------
            ||
            \/ 
       -------------
       | Filter 3  |   eliminates numbers that are multiples of 3
       -------------
            ||
            \/ 
       -------------
       | Filter 5  |   eliminates numbers that are multiples of 5
       -------------
            ||
            \/ 
       -------------
       | Filter 7  |   eliminates numbers that are multiples of 7
       -------------
            ...
The design of the actual program will depend also on how the control is organized, namely how the various objects get activated. In general we can distinguish two main possibilities:
  1. Single-thread of control: only one object is active at a time

  2. Multi-threads of control: many objects are (potentially) active at the same time.
For the moment we will consider the first possibility only. We may consider the second one later, when we will study Concurrent Programming in Java.

The sieve of Eratosthenes with a single-thread of control

We have two main possibilities for organizing the flow of control:
  1. Demand driven: Each component is activated by "demanding" from it the production of a new datum. More precisely, each component is activated by the next component in the chain. (The last one is activated by some external entity, for instance by a print command.) Thus the flow of control goes in the direction opposite to the flow of data.

  2. Data driven: Each component is activated by sending to it a new datum. More precisely, each component is activated by the previous component in the chain. (The first one keeps active by repeating some instructions in a loop.) Thus the flow of control goes in the same direction as the flow of data.

Demand driven model

This solution is the one which is illustrated in Sethi's book. The idea is to create three kinds of objects (using Sethi's terminology):
  1. a Counter, whose task is to produce, on-demand the next natural number,
  2. a Filter for each prime number k generated so far, whose task is to generate, on-demand, a new number candidate to be a prime number. When the filter receives a request from the next object in the chain, it repeatedly asks the previous object in the chain for new numbers, until it gets a number n which is not a multiple of k. (or, equivalently, of which k is not a factor). Such number n is returned, and all the others are eliminated.
  3. a Sieve, whose task is to generate, on-demand, a new prime number n (obtained by asking the last filter in the chain) and create a filter with factor n between itself and the last filter in the chain.
These objects are organized in a chain, starting from the Counter, then the Filters ordered by increasing factors, and finally the Sieve.

In this kind of architecture, each object must know the identity of the previous object in the chain (called source by Sethi). Thus we have a structure like the following (after the production of 5). The arrows indicate the pointers between objects.

       -------------
       |     6     |  Counter. Will generate 6 next       
       -------------
             ^
             |
       ------+------              
       |     |     |  
       |           |
       -------------
       |     2     |   Filter with factor 2
       -------------
             ^
             |
       ------+------              
       |     |     |  
       |           |
       -------------
       |     3     |   Filter with factor 3
       -------------
             ^
             |
       ------+------              
       |     |     |  
       |           |
       -------------
       |     5     |   Filter with factor 5
       -------------
             ^         ^
             |         |  flow of control
       ------+------              
       |     |     |   Sieve
       |           |
       -------------
            
Note that, at the beginning, there are only two objects: Sieve and Counter. This means that the source of Sieve is originally a Counter, and later it becomes a Filter. One advantage of using Object-Oriented technology is that we can treat the two situations uniformly. This can be done by using a basic class Item (following Sethi's terminology), of which all the other classes (Counter, Filter, and Sieve) will be subclasses. The main purpose of Item is to contain a declaration of a metod out(), (following Sethi's terminology) which is the method invoked by the next object in the chain when making a request for a new item. Thus each object in the chain can be seen as an Item by the next object; namely its particular subclass need not be known by the next object. Of course, the method out() will have to be overriden in all the subclasses.

Another advantage of OO is that we can declare a single destructor method in Item and have it effective, by inheritance, in all the subclasses.

We are now ready to give the code. This is essentially the same code defined in Sethi, with a few additions to illustrate its possible use.

/*   Eratosthenes' Sieve        */
/*   Demand driven model        */

#include <iostream.h> 

class Item { 
   protected:
      Item *source; 
   public:
      virtual int out() {} // to be overriden in the subclasses
      ~Item() { delete source; }
}; 

class Counter: public Item { 
      int value; 
   public: 
      int out() { return value++; } 
      Counter() { source = 0; value = 2; } 
}; 

class Filter: public Item { 
      int factor; 
   public: 
      int out() { 
          int n; 
          n = source->out(); 
          while(!(n%factor)) { n = source->out(); }
          return n; 
      } 
      Filter(Item *src, int f){ source = src; factor = f; }
}; 


class Sieve :public Item{ 
   public: 
       int out() { 
           int n = source->out(); 
           source = new Filter(source, n); 
           return n;
       } 
       Sieve(Item *src){source =src;}
};

main() {
        Counter *g = new Counter();
        Sieve *s = new Sieve(g);
        char a;
        cout << "Do you want the first prime number? (y/n) ";
        cin >> a;
        while (a=='y'){
           cout << s->out() << '\n';
           cout << "Do you want the next prime number? (y/n) ";
           cin >> a;
        };
        delete s;
}

Data driven model

Here we need only two kinds of objects:
  1. a Counter, whose task is to produce the sequence of natural numbers,
  2. a Filter for each prime number k generated so far, whose task is to eliminate the numbers (received from previous component in the chain) which are multiples of k. The last filter in the chain also creates the next filter.
Again, these objects are organized in a chain, starting from the Counter, then the Filters ordered by increasing factors.

In this kind of architecture, each object must know the identity of the next object in the chain, which we will call destination by Sethi). Thus we have a structure like the following (after the production of 5). The arrows indicate the pointers between objects.

       -------------
       |     6     |  Counter. Will generate 6 next       
       -------------
       |           |
       |     |     |
       ------+------              
             |        | flow of control
             V        V
       -------------
       |     2     |   Filter with factor 2
       -------------
       |           |
       |     |     |
       ------+------              
             |       
             V      
       -------------
       |     3     |   Filter with factor 3
       -------------
       |           |
       |     |     |
       ------+------              
             |       
             V      
       -------------
       |     5     |   Filter with factor 5
       -------------
       |     /     |
       |    /      |
       -------------              
            

We are now ready to give the code.

/*   Eratosthenes' Sieve      */
/*   Data driven model        */

#include <iostream.h> 
        
class Item { 
   protected:
      Item* destination; 
   public:
      ~Item(){ delete destination; }
      virtual void in(int n){} // to be overriden in the subclasses
};

class Filter: public Item {
      int factor;
   public:
      Filter(int f){ factor = f; destination = NULL; }
      void in(int n) {
           if (n%factor == 0) return;
           if (destination != NULL) destination->in(n);
           else {
                 destination = new Filter(n); 
                 cout << "The next prime number is " << n << '\n';
           }
      }
};

class Counter: public Item { 
      int value; 
      int max;
   public: 
      Counter(int m){ max = m; } 
      void start(){
           if (max < 2) return;
           destination = new Filter(2);
           cout << "The first prime number is " << 2 << '\n';
           value = 3;
           while (value <= max) destination->in(value++); 
      } 
}; 

class EratostenesSieve{
      int max;
      Counter* gen;
   public:
      EratostenesSieve(int m){ max = m; }
      void start(){     
           gen = new Counter(max);
           gen->start();
      }
      ~EratostenesSieve(){ delete gen; }
};

void main() {
        int max;
        cout << "Please input the max number to be generated" << '\n';
        cin >> max;
        EratostenesSieve es(max);
        es.start();
        cout << "End\n";
}