C++/CLI Interfaces & Generic Types

C/C++ Users Journal August, 2005

Multiple classes and a common set of capabilities

By Rex Jaeschke

Rex Jaeschke is an independent consultant, author, and seminar leader. He serves as editor of the standards for C++/CLI, CLI, and C#. Rex can be reached at rex@RexJaeschke.com.

This month, I examine how you define C++/CLI implementation contracts, called "interfaces."

Interfaces

Sometimes it is useful to have unrelated classes behave in similar ways by having them share a common set of public members. One way you can achieve this is to define them with a common base class; however, this approach is limiting because it requires that these classes be related via inheritance, yet they might each already have a base class of their own, and CLI types support single-class inheritance only.

C++/CLI provides a way for multiple classes to implement a common set of capabilities through what is called an "interface." An interface is a set of member function declarations. Note that the functions are only declared, not defined; that is, an interface defines a type consisting of abstract functions (which really are pure virtual functions), where those functions are implemented by client classes as they see fit. An interface allows unrelated classes to implement the same facilities with the same names and types without requiring those classes to share a common base class. Listing 1 shows how an interface is defined.

An interface definition looks much like that for a class, except that you use interface instead of ref or value, and the functions have no body and are implicitly public and abstract. By convention, an interface's name begins with the letter I, followed by a capital letter. (interface class and interface struct are equivalent.) Like a class, an interface can have public or private visibility.

An interface can have one or more "base interfaces," in which case, it inherits all abstract functions from those interfaces. For example, in Listing 2, interface I2 explicitly inherits from I1, while I3 explicitly inherits from I1 and I2, and implicitly inherits from I1 via I2.

A class implements an interface using the same notation as it does for deriving from a base class; see Listing 3.

A class can implement more than one interface, in which case, there is a comma-separated list of interfaces whose order is arbitrary. Of course, a class can have an explicit base class as well as implementing one or more interfaces, in which case, the base class is usually (but need not be) written first in the list.

If a class implements an interface, but does not define all of the functions from that interface, that class must be declared abstract. Of course, any class derived from that abstract class is also abstract unless it defines the previously abstract functions.

Interfaces do not provide multiple inheritance; a class can have only one base class. While interfaces provide some of the same capability as multiple-class inheritance, they really are quite different. For example, a class cannot inherit function definitions from an interface. The interface hierarchy is independent of the class hierarchy—classes that implement the same interface may or may not be related through the class hierarchy.

Listing 4 shows a second class, Queue, which is not related to List (except that, like all classes, both are rooted in Object), yet both classes implement the same interface.

Now you can write functions that deal with arguments that are either Lists or Queues, as in Listing 5.

In cases 1 and 2, you use c, a handle to an interface, to access the underlying List or Queue. Specifically, you can pass to ProcessCollection a handle to any object whose class implements this interface or that is derived from any class that implements this interface.

Listing 6 shows that an interface can also include properties. X is a read-only property, Y is a write-only property, while Z is a read-write property. In the case of a read-write property, the get and set accessors can be declared in either order.

The members of an interface can also be static data members, instance or static functions, a static constructor, instance or static properties, instance or static events, operator functions, or nested types of any kind.

The for each statement enumerates the elements of a collection, executing an embedded statement for each element of that collection, using the syntax:

for each (type identifier in expression)
   embedded-statement

The type of expression must be a "collection type." To be a collection type, a type must implement the interface System::Collections::IEnumerable, defined in Listing 7.

As you can see, GetEnumerator returns a handle to an IEnumerator, defined in Listing 8.

The type System::Array is a collection type, and because all CLI array types derive from System::Array, any array type expression is permitted as the expression in a for each statement. In case 1 of Listing 9, for each is used to traverse an array of int, and the same process is repeated in case 2, using an enumerator directly.

Generic Types

Just as a function can be defined with one or more type placeholders, so too can a type. Consider a type that models a list of elements, with each element being accessed using subscript notation. Such a type is often referred to as "vector." It is quite straightforward to implement a vector to hold a set of ints, a set of doubles, or a set of elements of some user-defined type. However, because the code for each typed implementation is identical except for the element type, you can use the generic type machinery to define one vector type, and then create specific typed instances of that generic type. Listing 10 contains an example.

As with a generic function, a generic type declaration begins with generic <typename t1, ..., typename tn>, which introduces the type parameters t1 through tn. The scope of these parameters is through the end of the type definition to which they are attached. In this example, there is only one type parameter, T.

As you can see in case 1, a Vector is really stored as an array of elements whose type is T.

In case 2, I define a default indexed property that lets you subscript a Vector with an int index. Of course, you set and get elements of type T, whatever that type might be, at runtime.

Rather than access the private members directly, access is obtained via the public properties. For example, Length is used instead of length, and to subscript the current Vector in case 3, you use this[i]. You might be tempted to use the for each loop, as in case 4, instead of the plain old for loop. However, that wouldn't work. In a for each loop, a copy of each element is made available via the local variable named in that loop construct; that is, that variable is not the original element. As such, changing its value does not change the value of the element of which it was a copy.

In case 5, you need to set each element to its default value. Fortunately, Standard C++ requires that every type have a default constructor—even for nonclass types—that is represented as the type name followed by empty parentheses.

In the case of a constructor having no parameters (case 6), we allocate an array of zero elements. (Note that a handle to an array of zero elements is different from a handle containing nullptr.) You do this so that the member functions work correctly even for empty Vectors.

For completeness, you define a destructor that sets the storage handle to nullptr, explicitly telling the garbage collector that you are done with this object and its associated memory.

The uses of this[i] in cases 8 and 9 appear quite innocent; however, what is really happening here? Unlike an equivalent template, generic class Vector is compiled to an assembly without type T's being known. If the compiler doesn't know the type of T, it doesn't know the type of this[i], so how can it generate code to convert this expression to the correct thing so that Concat will work as expected? It doesn't have to know! One of the overloads for Concat expects type Object^ for its second argument, and because this[i] has type T, and whatever type T is at runtime, it is guaranteed to be derived from System::Object, so it is a compatible argument type. Moreover, because System::Object has a virtual function called ToString, a call to that virtual function results in a String^, and assuming that type T has overridden that function, the correct string will be returned.

Function Equals is straightforward with only one thing needing to be pointed out, and this is case 11. The intuitive thing to write is the test using the equality operator, !=; however, that will not compile. Remember, class Vector is compiled without knowing anything about the type of T, except that it's ultimately derived from System:: Object. As such, the only member functions it allows to be called on a T are those provided by System::Object, and that type does not define operator!=. However, fortunately, it does provide function Equals, so you can use that to achieve the desired result. Then, presuming type T overrides that function to perform equality on two Ts, it all works perfectly. Listing 11 is the main application program.

To create a specific type of Vector, you specify that type as a type argument inside the angle brackets, as with int in cases 1 and 3. Remember, int is a synonym for the value class type System::Int32. (If the generic type has multiple type parameters, a corresponding comma-separated list of type arguments is needed.)

In cases 4 and 5, I define a Vector of String^, a ref class type. In cases 6 and 7, I define a Vector of DateTime, a value class type. In case 8, I define a Vector whose elements are each (different length) Vectors of int. Finally, I test the Equals function using a variety of different length and value int Vectors. Figure 1 shows the output produced.

Just as we can define generic classes, so too can we define generic interfaces and delegates.

Earlier, I pointed out the restriction on what you can call from a generic type's member function. By default, a type argument is constrained to be any type derived from System::Object, which is, of course, any CLI type. You can supply one or more constraints on each type parameter in a generic type or function via a where clause, as in Listing 12.

In this example, the compiler will allow any type argument that has the type System::ValueType or has a type derived from that type to be passed to the generic type. Given the attempts at defining a particular type from this generic type, cases 2 and 3 are rejected because String and Vector<int> are not value class types. Likewise, given the constraints in Listing 13, case 5 is also rejected, as the value class type C does not implement the interface System::IComparable while System::Int32 and System::DateTime do.

Once the compiler knows that T can be constrained to something more specific than System::Object, it can allow calls to member functions from the constraint type(s) (which can include one base type and any number of interface types, in any order).

Reader Exercises

To reinforce the material I've presented, try the following activities:

  1. The namespaces System and System::Collections contain quite a few interface types, all having names beginning with "I." Find out what their names are and get a feel for the purpose of each of these types.
  2. By default, when a generic class is compiled, nothing is known about the type of its type arguments except that they are ultimately derived from System::Object. Considering this, can the complex number type, Complex (from the May 2005 CUJ installment) be implemented as a generic class such that float and double instances, for example, can be created? Specifically, how would you implement the operator+ and operator- functions generically? After all, you can't do arithmetic on an unknown type!
  3. The CLI Standard Library defines a number of generic types, including in the namespace System::Collections::Generic the following: Dictionary<TKey,TValue>, IDictionary<TKey,TValue>, IEnumerable<T>, IEnumerator<T>, IList<T>, and List<T>. (Microsoft's .NET product provides many more.) Action<T> and Predicate<T> are generic delegate types in namespace System. Look at the documentation for these types, and look for other generic types in the library.
  4. Look at the name of a generic type. Define three generic ref classes called G1, G2, and G3, have one, two, and three type parameters, respectively. See how these three types are distinguished from one another even though they all have the same name.