TStringList as a template array class

By David Bridges

There are many container and array classes available for use with C++ Builder, such as the ones in the Standard Template Library (STL) that ships with all versions. Often, however, I like a “quick and easy” alternative to using these classes, which frequently have a high overhead of coding, and impose requirements on the type of objects that you put into an array.

 

C++ Builder’s Visual Component Library contains a few types of simple array classes, and among them is TStringList. TStringList is ideal for storing an array of strings. It has built-in methods for adding, deleting, sorting, finding, etc. One important feature that it also has is the ability to store an additional user-defined value along with each string. This value is accessed via the Objects property, which works just like the Strings property except that it returns objects of type TObject*.

 

The Objects property greatly extends the capabilities of TStringList. Because you can store pointers to any type of object, TStringList can be used as a container class for any type of object you can think of. However, there are a few drawbacks to using TStringList this way. First, in order to store and then retrieve your object pointers using the Objects property, you must always cast the pointers back and forth from between your type and TObject*. Second, there is no ownership defined for the objects in a TStringList. This means that when the TStringList object is deleted, any associated pointers contained in Objects are not deleted. You must manually delete all objects yourself in order to avoid a memory leak. For example: 

class MyClass
{
  public:
  AnsiString sValue;
  MyClass(AnsiString sInValue) { sValue = sInValue; }
};

// Create the list
TStringList* mylist = new TStringList();

// Add objects to the list
mylist->AddObject("1", (TObject*) new MyClass("First"));
mylist->AddObject("2", (TObject*) new MyClass("Second"));

// Read objects from the list
for (int i=0;i< mylist->Count;i++) {
  MyClass* nextobj = (MyClass*)(mylist->Objects[i]);
  MessageBox(NULL, nextobj->sValue.c_str(), "Next Object:", MB_OK);
}

// Delete the objects
for (int i=0;i< mylist->Count;i++) {
  MyClass* nextobj = (MyClass*)(mylist->Objects[i]);
  delete nextobj;
}

// Free the list
delete mylist;

As you can see, the code isn’t very pretty. We continually have to cast back and forth between TObject and the actual class we’re using for the objects. Also, we must manually delete all of the objects when we’re done with them.

 

But we do get some good benefits from using the TStringList class: we can sort the list based on the string values (“1” and “2”, in this case), we can iterate through the list, and use all of the other methods and properties of TStringList.

 

The ObjectList class

The ObjectList class solves the casting problem and the deleting problem. One of my requirements in designing this class is that it imposes no restrictions upon the class of objects that it holds. The class doesn’t have to be derived from TObject, doesn’t have to have any operators defined, etc. In fact, it can be absolutely any type you want. Also, it should have a simple way to handle ownership of its objects. The listing for this class is shown in Listing A.

 

Before explaining any further, let’s revisit the first example, but using the ObjectList class instead: 

typedef ObjectList<MyClass> MyObjectList;

MyObjectList* myobjects = new MyObjectList(true);

// Add objects to the list
myobjects->AddItem("1", new MyClass("First"));
myobjects->AddItem("2", new MyClass("Second"));

// Read objects from the list
for (int i=0;i<myobjects->Count;i++) {
  MyClass* nextobj = myobjects->Items[i];
  MessageBox(NULL, nextobj->sValue.c_str(), "Next Object:", MB_OK);
  }

// Free the list, which will delete all objects.
delete myobjects;

The AddItem() method replaces the AddObject() method of TStringList. It takes a string and a pointer to an object of the class used in the template definition (in this case, MyClass). There is also a method called AddNewItem(). This works exactly the same as AddItem(), but it adds a new copy of the original object to the list, instead of the object itself. This sometimes saves writing code to allocate a new object each time. The only restriction is that the object should have a copy constructor.

 

The Items property replaces the Objects property of TStringList. It returns an object of the desired class, instead of a TObject pointer.

 

As you can see from the class constructor, the default behavior of the class is to sort the list and accept duplicates. It will sort based on whatever you have supplied as the string parameter in the AddItem() method. This gives a lot of flexibility since you can generate any kind of sort key you want for an object (or have the object create its own).

 

The ObjectList class has the ability to take ownership if the objects it contains. The class constructor takes one parameter, which is a flag indicating whether the list owns its objects. If it owns the objects, they will be automatically deleted when the list is deleted. If not, then you are responsible for deleting them. You can always override this ownership flag however, by using the ClearItems() method. If you pass true to ClearItems(), the list will be emptied and each of it’s objects will be deleted. Passing false to ClearItems() will empty the list without deleting the objects.

 

One final thing to mention is that this provides a good example of creating properties in a class—in this case, the Items property. This is a very typical example of a read/write property and its associated read and write methods. This happens to be an indexed property so the read method looks like this:

 

T* GetItem(int index)

 

The write method is declared like this:

 

void SetItem(int index, T* ptr)

 

If it were not an indexed property, the read and write methods would be identical but lacking the index parameters. The __property keyword ties these together and specifies that these functions actually get called when you read or write the Items property.

 

Conclusion

By using the built-in power of the TStringList class and extending it by creating a template class that can handle any data type, we have created a very capable array class that can take advantage of all of the common array functions. 

template <class T> class ObjectList : 
  public TStringList
{
  public:

  // Set OwnsObjects to true if the objects should automatically be 
  // deleted when the list is destroyed.
  bool OwnsObjects;

  __fastcall ObjectList(bool bOwnsObjects = false) : TStringList()
  {
    Duplicates = dupAccept;
    Sorted = true;
    OwnsObjects = bOwnsObjects;
  }

  __fastcall ~ObjectList()
  {
    ClearItems(OwnsObjects);
  }

  int AddItem(AnsiString sValue, T* ptr)
  {
    int index = Add(sValue);
    Objects[index] = (TObject*)ptr;
    return index;
  }

  int AddNewItem(AnsiString sValue, T* ptr)
  {
    T* newptr = new T;
    *newptr = *ptr;
    return AddItem(sValue, newptr);
  }

  T* GetItem(int index)
  {
    return (T*)Objects[index];
  }

  void SetItem(int index, T* ptr)
  {
    Objects[index] = (TObject*)ptr;
  }

  void ClearItems(bool Delete)
  {
    if (Delete) {
      for (int i=0;i<Count;i++) {
        T* ptr = GetItem(i);
        if (ptr) {
          delete ptr;
          SetItem(i, NULL);
        }
      }
    }
    Clear();
  }

  __property T*  Items[int Index] = {read=GetItem,write=SetItem};

};