Click here to Skip to main content
Click here to Skip to main content

Implementing custom collection classes with MC++

, 12 Jun 2002
Rate this:
Please Sign up or sign in to vote.
Tutorial on creating your own collection classes that are enumerable and sortable. Explains in detail the usage of the IEnumerable, IEnumerator, IComparable and IComparer interfaces

Introduction

A collection class is basically a pool of indexed objects. This means we know how many objects the collection has at any one time. We can enumerate the objects one by one. We can access the object associated with a particular index. Usually collection classes are also sortable. For collection classes to be sortable, the objects it holds or collects must be comparable among objects of it's same type. In this tutorial we'll see how to design and create collection classes of our own. We'll create a class called Student which implements the IComparable interface. This will serve as the object we collect. We'll also have a nested class inside the Student class called StudentAgeComparer which implements the IComparer interface. This class is for sorting purposes. Then we'll create the class called StudentCollection which is the collection class for the Student class and which implements the IEnumerable interface. The StudentCollection class will hold a nested class called StudentEnumerator which implements the IEnumerator interface. This is the interface that implements all the required methods and properties that makes a class enumerable. By the way C# programmers might be interested to know that they can use foreach to enumerate collection classes that implement IEnumerable and IEnumerator interfaces.

The IEnumerable interface

IEnumerable is an interface that a collection class implements if it's members are to be enumerated. The foreach C# keyword can enumerate only those classes that implement IEnumerable. It's sole purpose is to expose the enumerator object which is used to enumerate the various objects in the collection in a sequential order. This enumerator object is nothing but a pointer to an instance of a class that implements the IEnumerator interface. Typically when we have a class that implements the IEnumerable interface we have an inner class that acts as the enumerator class of this class. The inner enumerator class will implement IEnumerator.

The IEnumerable interface defines just one single method called GetEnumerator which simply returns the enumerator for the class. A typical scenario is shown below where we have a collection class called AbcCollection which implements IEnumerable and a nested inner class called AbcCollectionEnumerator which implements IEnumerator. The advantage of making the IEnumerator implementing class a nested inner class of the IEnumerable implementing class is that the enumerator will have access to the private fields and methods of the collection class.

__gc class AbcCollection : public IEnumerable 
{
public:
    IEnumerator* GetEnumerator()
    {
        ...
    }

    __gc class AbcCollectionEnumerator : public IEnumerator
    {
        ...
    };
};

The IEnumerator interface

Any collection class that allows it's collected items to be enumerated must implement an enumerator which implements the IEnumerator interface. The enumerator is initially positioned just before the first item. There is a method called MoveNext which advances the enumerator to the next object in the collection. If the position of the enumerator was successfully advanced MoveNext returns true. If MoveNext failed because the enumerator has reached the end of the collection, then it returns false. If the collection has been modified after the enumerator was instantiated then MoveNext will throw an InvalidOperationException exception.

The IEnumerator interface has a property called Current which returns the current object in the collection that the enumerator is pointing to. If the enumerator is positioned before the first element in the collection or if the enumerator has enumerated past the collection's edge, then an InvalidOperationException exception is thrown. An InvalidOperationException exception is also thrown if the collection has been modified after the enumerator has been instantiated. The third and last member IEnumerator implements is the Reset method which will reset the enumerator to it's initial position of one before the first object in the collection. Typically a class that implements the IEnumerator interface has a constructor that takes an argument. This argument will be of the type of the collection class, that this class enumerates. There will also be a private member of the collection class type and the constructor will initialize it with the argument passed to it.

__gc class AbcCollectionEnumerator : public IEnumerator
{
    AbcCollectionEnumerator(AbcCollection* abc)
    {
        m_privmember = abc;
        ...
    }
    ...
};

The IComparable interface

Any class that needs to be ordered or sorted will need to implement IComparable. That is not strictly true in the sense that another class can be used as a comparer for objects of our class and this comparer class will need to implement the IComparer interface. But we'll touch that later. Anyway IComparable defines only one member which is a method called CompareTo. CompareTo will take an object of the same type as the class that implements IComparable and will return an int. The returned int will be 0 if the object is equal to the current instance, a negative number if the current instance is less than the object and a positive non-zero number if the current instance is greater than the object. An array or array list of objects that implement IComparable are sortable.

The IComparer interface

The IComparer interface provides a method to compare two objects and is used for sorting collection classes. One of the overrides of the Sort() functions take an IComparer object as argument and will use this IComparer to compare the objects that are to be sorted. The IComparer interface defines only one method called Compare. Similar to the CompareTo method of the IComparable interface, this returns 0 if both objects are equal. It will return a negative number if the first object is less than the second object and a non-zero positive number if the first object is greater than the second object. Typical implementations are done using the CompareTo method of one or more of the members of the objects to be compared.

Student class

The Student class that we are going to implement will implement IComparable so that Student objects may be compared and sorted. In addition we'll also have an inner nested class called StudentAgeComparer which provides an alternate sorting based on an alternate criteria from the default one. The Student class is listed below in an incomplete manner to save space. To see the complete listing you can take a look at the source code provided with the article or the full code listing provided at the end of the article.

__gc class Student : public IComparable
{
public:
    Student(String* sname, String* srollnum, int sage)
    {
        ...
    }

    void Display()
    {
        ...
    }

    int CompareTo(Object* obj)
    {
        Student* tmp = dynamic_cast<Student*>(obj);
        return m_name->CompareTo(tmp->m_name);
    }

    __gc class StudentAgeComparer : public IComparer 
    {
        ...
    };

private:
    String* m_name;
    String* m_rollnum;
    int     m_age;
};

As you can see, we don't do anything fancy in the CompareTo method. We simply delegate the comparison to a lower level by calling CompareTo on the m_name member of the object which is a String. The String class implements IComparable and thus we are saved some coding, which is excellent. Thus by default a Student object collection will be sorted on the basis of the Student object's m_name member. Now assume that someone says that they want to sort a Student object collection or array by age. To facilitate for such a contingency we have an inner class called StudentAgeComparer which implements the IComparer interface. We'll see it's implementation below.

__gc class StudentAgeComparer : public IComparer 
{
public:
    int Compare(Object* x,Object* y)
    {
        Student* x1 = dynamic_cast<Student*>(x);
        Student* y1 = dynamic_cast<Student*>(y);
        return x1->m_age.ToString()->CompareTo(y1->m_age.ToString());
    }
};

Again we delegate the comparison to a lower level. This time we call the CompareTo function on the String returned by calling ToString() on the m_age member. We could have called it directly on the int but then we'd have had to do boxing on our own, this is MC++ remember, not C#. I thought it best to use the String forms to do the comparison. Later on when we implement the collection, we'll see how we implement an alternate sort that uses the StudentAgeComparer to do sorting based on a student's age instead of on a student's name which is the default.

StudentCollection class

The StudentCollection class is a collection class of Student objects and implements IEnumerable. It also has an inner class called StudentEnumerator which acts as the enumerator for the StudentCollection class. The StudentCollection class has a private ArrayList member which is used to store the various Student objects. I chose an ArrayList instead of an Array because an ArrayList is dynamically resizable. Thus I don't need to specify a default size. The ArrayList will keep growing as I keep adding more objects. The StudentCollection is listed below and as before the listing is only partial. Look at the zipped source code or the full listing at the end of the article for the complete listing of code.

__gc class StudentCollection : public IEnumerable
{
public:
    StudentCollection()
    {
        m_students = new ArrayList();
        m_dirty = false;
    }


    __gc class StudentEnumerator : public IEnumerator
    {
        ...
    };


    IEnumerator* GetEnumerator()
    {
        m_dirty = false;
        return dynamic_cast<IEnumerator*>(new StudentEnumerator(this));
    }

    int Add(Object* value)
    {
        m_dirty = true;
        return m_students->Add(value);
    }

    void Sort()
    {
        m_dirty=true;
        m_students->Sort();
    }

    void SortByAge()
    {
        m_dirty=true;
        m_students->Sort(new Student::StudentAgeComparer());
    }

private:
    ArrayList* m_students;
    bool m_dirty;
};

The GetEnumerator() method returns a new instance of the StudentEnumerator class. Thus each time we call GetEnumerator() a new enumerator is created. If any change is made to the collection we set a dirty flag to true to indicate to the enumerator that the collection has been changed after the enumerator has been created. There are two Sort methods provided. The first Sort method simply calls the zero-argument Sort overloaded method of the ArrayList class. In this case comparison and sorting can be done on the Student object because it implements IComparable. There is also an alternate sorting method called SortByAge where we call another overload of ArrayList's Sort() method where we pass it an instance of an object that implements the IComparer interface. In this case we pass a new instance of the StudentAgeComparer class. Now the relevance of the IComparable and the IComparer interfaces must have become clearer to you. Now let's take a look at the StudentEnumerator class.

__gc class StudentEnumerator : public IEnumerator
{
public:
    StudentEnumerator(StudentCollection* sc)
    {
        m_sc = sc;
        index = -1;
    }

    Object* get_Current()
    {
        if(index < 0 || index >= m_sc->m_students->Count || m_sc->m_dirty)
            throw new InvalidOperationException();

        return m_sc->m_students->Item[index];
    }

    void Reset()
    {
        m_sc->m_dirty = false;
        index = -1;
    }

    bool MoveNext()
    {
        if(m_sc->m_dirty)
            throw new InvalidOperationException();

        index++;

        return (index < m_sc->m_students->Count);
    } 

private:
    int index;
    StudentCollection* m_sc;
};

The enumerator class has a private member of type StudentCollection and in the constructor we assign the passed StudentCollection object to this private member. We also have an index which is by default set to -1 since the ArrayList index starts from 0. When a call is made to the Reset method we simply set the index back to -1 and set the dirty flag to false. In the MoveNext() method we throw an InvalidOperationException exception if the dirty flag is set. Else we increment index and if we haven't gone past the end of the collection, we return true, otherwise we return false. Similarly in the get implementation of the Current property, we check to see if the index is below 0 or if it has crossed the edge of the collection's limit. In either case or if the dirty flag has been set an exception of type InvalidOperationException is thrown. Otherwise the Student object in the ArrayList that has the current index is returned.

Enumerating our collection

StudentCollection* sc = new StudentCollection();

//Populate the collection

IEnumerator* iEnum = sc->GetEnumerator();

while(i->MoveNext())
{
    Student* stmp = dynamic_cast<Student*>(iEnum->Current);
    stmp->Display();
}

Screenshot of sample

Full source listing

#include "stdafx.h"

#using <mscorlib.dll>
#include <tchar.h>

using namespace System;
using namespace System::Collections;

__gc class Student : public IComparable
{
public:
    Student(String* sname, String* srollnum, int sage)
    {
        m_name = sname;
        m_rollnum = srollnum;
        m_age=sage;
    }

    void Display()
    {
        Console::WriteLine("{0}{1}{2}",
            m_name->PadRight(30),
            m_rollnum->PadRight(15),m_age.ToString());
    }

    int CompareTo(Object* obj)
    {
        Student* tmp = dynamic_cast<Student*>(obj);
        return m_name->CompareTo(tmp->m_name);
    }

    __gc class StudentAgeComparer : public IComparer 
    {
    public:
        int Compare(Object* x,Object* y)
        {
            Student* x1 = dynamic_cast<Student*>(x);
            Student* y1 = dynamic_cast<Student*>(y);
            return x1->m_age.ToString()->CompareTo(
                        y1->m_age.ToString());
        }
    };

private:
    String* m_name;
    String* m_rollnum;
    int m_age;
};

__gc class StudentCollection : public IEnumerable
{
public:
    StudentCollection()
    {
        m_students = new ArrayList();
        m_dirty = false;
    }


    __gc class StudentEnumerator : public IEnumerator
    {
    public:
        StudentEnumerator(StudentCollection* sc)
        {
            m_sc=sc;
            index = -1;
        }

        Object* get_Current()
        {
            if(index <0 || index >= m_sc->m_students->Count ||
                m_sc->m_dirty)
                throw new InvalidOperationException();
            return m_sc->m_students->Item[index];
        }

        void Reset()
        {
            m_sc->m_dirty = false;
            index=-1;
        }

        bool MoveNext()
        {
            if(m_sc->m_dirty)
                throw new InvalidOperationException();
            index++;

            return (index < m_sc->m_students->Count);
        }

    private:
        int index;
        StudentCollection* m_sc;
    };


    IEnumerator* GetEnumerator()
    {
        m_dirty = false;
        return dynamic_cast<IEnumerator*>(
                    new StudentEnumerator(this));
    }

    int Add(Object* value)
    {
        m_dirty = true;
        return m_students->Add(value);
    }

    void Sort()
    {
        m_dirty=true;
        m_students->Sort();
    }

    void SortByAge()
    {
        m_dirty=true;
        m_students->Sort(new Student::StudentAgeComparer());
    }

private:
    ArrayList* m_students;
    bool m_dirty;
};


int _tmain(void)
{ 
    Student *s1 = new Student("Sally Smith","GF/45/112",22);
    Student *s2 = new Student("Alice Brown","GF/45/117",27);
    Student *s3 = new Student("Linda Mathews","GF/45/120",23);

    StudentCollection* sc = new StudentCollection();
    sc->Add(s1);
    sc->Add(s2);
    sc->Add(s3);

    IEnumerator* iEnum = sc->GetEnumerator();

    while(iEnum->MoveNext())
    {
        Student* stmp = dynamic_cast<Student*>(iEnum->Current);
        stmp->Display();
    }

    sc->Sort();
    iEnum->Reset();
    Console::WriteLine();    
    
    while(iEnum->MoveNext())
    {
        Student* stmp = dynamic_cast<Student*>(iEnum->Current);
        stmp->Display();
    }

    sc->SortByAge();
    iEnum->Reset();
    Console::WriteLine();    
    
    while(iEnum->MoveNext())
    {
        Student* stmp = dynamic_cast<Student*>(iEnum->Current);
        stmp->Display();
    }

    return 0;
}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Nish Sivakumar

United States United States
Nish is a real nice guy who has been writing code since 1990 when he first got his hands on an 8088 with 640 KB RAM. Originally from sunny Trivandrum in India, he has been living in various places over the past few years and often thinks it’s time he settled down somewhere.
 
Nish has been a Microsoft Visual C++ MVP since October, 2002 - awfully nice of Microsoft, he thinks. He maintains an MVP tips and tricks web site - www.voidnish.com where you can find a consolidated list of his articles, writings and ideas on VC++, MFC, .NET and C++/CLI. Oh, and you might want to check out his blog on C++/CLI, MFC, .NET and a lot of other stuff - blog.voidnish.com.
 
Nish loves reading Science Fiction, P G Wodehouse and Agatha Christie, and also fancies himself to be a decent writer of sorts. He has authored a romantic comedy Summer Love and Some more Cricket as well as a programming book – Extending MFC applications with the .NET Framework.
 
Nish's latest book C++/CLI in Action published by Manning Publications is now available for purchase. You can read more about the book on his blog.
 
Despite his wife's attempts to get him into cooking, his best effort so far has been a badly done omelette. Some day, he hopes to be a good cook, and to cook a tasty dinner for his wife.

Comments and Discussions

 
GeneralInherit IEnumerable in C++ CLI PinmemberMikeTheEvilTwin23-Jan-06 10:17 
AnswerRe: Inherit IEnumerable in C++ CLI Pinmemberzaccheus5-May-06 11:52 
GeneralGreat article Pinmemberfinoci26-Aug-05 6:11 
Questionmultiple classes in arraylist? Pinmemberalain cheng6-Jun-04 22:54 
GeneralThanks nish! PinmemberMartin Häsemeyer4-Oct-03 22:25 
Generalbug in MoveNext PinmemberJBoschen31-May-03 17:21 
this is a bug...
 
bool MoveNext()
{
if(m_sc->m_dirty)
throw new InvalidOperationException();

index++;
 
return (index < m_sc->m_students->Count);
}

 
Consider calling MoveNextenough so that index rotates back around to a valid number. (even worse, since it's signed, it goes past the max positive value and is now negative, thereby satisifying the condition)
 
index shouldn't be incremented unless the function is returning true
 
Small bug, yes, but things like this creep into production code and clients somehow find them.
 
Something like this is safe,
return ( index < m_sc->m_students->Count ? ++index, true : false );

GeneralException PinmemberKarstenK28-Nov-02 4:26 
GeneralRe: Exception PineditorNishant S28-Nov-02 14:39 
QuestionWhat about STL? PinmemberJason Orphanidis26-Jun-02 21:23 
AnswerRe: What about STL? PinmemberChristian Graus26-Jun-02 21:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 13 Jun 2002
Article Copyright 2002 by Nish Sivakumar
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid