Introduction to C++/CLI Generics

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Before version 2.0, the .NET framework supported the Universal Type Container Model, in which objects are stored in a uniform manner. The universal type container in the Common Type System is Object and all types are derived either directly or indirectly from it. Version 2.0 of the framework supports a second model, called the Type Parameter Model, in which the binding of type information associated with an object is delayed, information that can vary from one invocation to another being parameterized. C++ supports this parameterized model for implementing templates. .NET framework 2.0 brings something similar, and it’s called generics.

C++/CLI supports both templates and generics for defining parameterized reference and value; and interface classes, functions, and delegates. This article is focused on C++/CLI generics, presented in comparison with templates. Unfamiliarity with templates should not be a problem for understanding generics.

Parameterized List

Type parameters

Templates type parameters are introduced either with the class or typename keyword (there is no syntactical difference):


template <class T>
ref class tFoo1
{
};

template <typename T>>
ref class tFoo2
{
};

The same applies to generics:


generic <class T>
ref class gFoo1
{
};

generic <typename T>
ref class gFoo2
{
};

The type placeholder (T in this example) is to be replaced with a user-specified type argument. Instantiation syntax is the same, both for templates and generics:


// templates
tFoo1<int>^ tfoo1;
tFoo2<String^>^ tfoo2;

// generics
gFoo1<int>^ gfoo1;
gFoo2<String^>^ gfoo2;

What is different is the time of the instantiation. Templates are instantiated at compile time, when the compiler constructs a new type by inserting the provided type into the type placeholder (in this example, there will be two types constructed from tFoo1, and two types constructed from tFoo2—one using type int and one using type String). Generics instantiation is done by the CLR at runtime, which constructs the type-specific instances by modifying the general syntax, depending on whether the type argument is a value or a reference type.

Templates support expressions, template parameters, and default parameter values. These are not supported by generics.

Non-type parameters and default parameter value

Templates enable you to supply a default value for both type and non-type parameters. Here is a template stack that specifies a default value for the size of the stack:


template <class T, int size = 128>
ref class tStack
{
array<T>^ m_stack;
int m_pointer;
public:
tStack()
{
m_stack = gcnew array<T>(size);
m_pointer = -1;
}
};

tStack<int, 10>^ tiStack = gcnew tStack<int, 10>;

Non-type parameters are not supported by generics. An attempt to use one will raise an error:


generic<class T, int size>
ref class gStack
{
};

To achieve the same thing with generics, you must pass the size to the stack constructor:


ref struct StackException: public System::Exception
{
System::String^ message;

public:
StackException(System::String^ msg):message(msg)
{}

System::String^ GetMessage() {return message;}
};

generic<class T>
ref class gStack
{
array<T>^ m_stack;
int m_pointer;
int m_size;
public:
gStack(int size)
{
m_size = size;
m_stack = gcnew array<T>(size);
m_pointer = -1;
}

void Push(T elem)
{
if(m_pointer < m_size-1)
{
m_stack[++m_pointer] = elem;
}
else
throw gcnew StackException(gcnew System::String(“Full stack”));
}

T Pop()
{
if(m_pointer >=0 )
{
T elem = m_stack[m_pointer];
–m_pointer;
return elem;
}
else
throw gcnew StackException(gcnew System::String(“Empty stack”));
}

bool IsEmpty()
{
return (m_pointer == -1);
}
};

gStack<String^>^ sStack = gcnew gStack<String^>(10);

The result of both code samples is a stack with space for 10 elements.

The default value also can be specified for the type parameters with templates:


template <class T = int, int size = 128>
ref class tStack
{
};

A similar attempt with generics will raise errors:


generic<class T = int> // this is not allowed
ref class gStack
{
};

Template Parameters

Templates support template parameters:


template <class T>
ref class tFoo1
{
};

template <template <class T> class Foo, class Type>
ref class tFoo2
{
Foo<Type> foo;
};

tFoo2< tFoo1, int >^ tfoo2;

An attempt to construct a similar parameterized type with generics will raise errors:


generic <generic <class T> class Foo, class Type> // not allowed
ref class gFoo2
{
};

Constraints

Though generally speaking, by using templates you can create parameterized types supporting an unlimited number of types. This holds true only if this means storing and retrieving objects of a parameter type (like in the stack example above, where objects of any type can be added and removed from the stack). When you need to manipulate these objects, for instance calling their methods, you introduce implicit constraints; these limit the number of types that can be used with the template.

Given the template class Foo below,


template <class T>
public ref class Foo
{
T object;
public:
Foo()
{
object.Init();
}
}

by calling Init() for an object, you limit the instantiation of this class only to types that have a method called Init() (which can be called from this context). An attempt to use int, for example, to instantiate it will be flagged as an error:

Foo<int> foo;

A constraint is introduced in this case too:


template <class T>
ref class Foo
{
T::X object;
};

Class Foo can be parameterized only with types that contain an inner type (accessible from this context) called X.

All these constraints are implicit constrains, and templates support no formal syntax to describe them. Templates are bound at compile time, when the correctness of template instantiation must be known. Generics are bound at runtime, but there is a need for a compile time mechanism to check the validity of runtime binding, and prevent the building of the program if the instantiation type does not match the specified prerequisites.

Look at this code before going further:


interface class IShape
{
public:
void Draw();
};

ref class Circle: public IShape
{
Point m_center;
double m_radix;
public:
Circle(Point^ pt, double radix):m_center(*pt), m_radix(radix)
{}

virtual void Draw()
{
System::Console::WriteLine(“Drawing a circle at {0} with
radix {1}”, m_center.ToString(),
m_radix);
}
};

ref class Brick
{
public:
void Draw()
{
System::Console::WriteLine(“Drawing a brick”);
}
};

Here, you have two reference types, Circle and Brick. They both provide a method called Draw() that takes no parameter and return void, but Circle implements the IShape interface. Now, assume you need a container for all these shapes, to which you can add elements and that provides a method that processes all contained shapes by drawing them.


generic<class T>
ref class Aggregator
{
System::Collections::Generic::List<T>^ m_list;
public:
Aggregator()
{
m_list = gcnew System::Collections::Generic::List<T>;
}

void Add(T elem)
{
m_list->Add(elem);
}

void ProcessAll()
{
for each(T elem in m_list)
{
elem->Draw();
}
}
};

Aggregator<IShape^>^ agr = gcnew Aggregator<IShape^>;

agr->Add(gcnew Circle(gcnew Point(0,0), 1));
agr->Add(gcnew Circle(gcnew Point(1,1), 2));
agr->Add(gcnew Circle(gcnew Point(2,2), 3));

agr->ProcessAll();

delete agr;

Compiling this code, there are several errors raised, saying that ‘Draw’ : is not a member of ‘System::Object’. What happens is that, by default, with the lack of explicit constraint specifications, the compiler assumes T is of type System::Object, which doesn’t have a method called Draw().

To address this problem, you introduce an explicit constraint, specifying that T can only be a type that implements the IShape interface. Constraints are introduced with the non-reserved word “where”.


generic<class T>
where T: IShape
ref class Aggregator
{
};

Now, you can add shapes to the container, as long as they implement IShape.

A container for bricks cannot be created as long as Brick does not implement IShape interface, because Brick does not meet the constraint:

Aggregator<Brick^>^ agr1 = gcnew Aggregator<Brick^>;

If we change Brick to:


ref class Brick: public IShape
{
public:
virtual void Draw()
{
System::Console::WriteLine(“Drawing a brick”);
}
};

we can have a brick-only container,


Aggregator<Brick^>^ agr = gcnew Aggregator<Brick^>;
agr->Add(gcnew Brick());
agr->Add(gcnew Brick());
agr->Add(gcnew Brick());

agr->ProcessAll();

or a container with both circles and bricks:


Aggregator<IShape^>^ agr = gcnew Aggregator<IShape^>;

agr->Add(gcnew Circle(gcnew Point(0,0), 1));
agr->Add(gcnew Brick());
agr->Add(gcnew Circle(gcnew Point(1,1), 2));
agr->Add(gcnew Brick());

agr->ProcessAll();

The following applies to generic constraints:

  • For a type to be a constraint, it must be a managed, unsealed reference type or interface. Classes from namespaces System::Array, System::Delegate, System::Enum, and System::ValueType cannot appear in the constraint list.


    public ref class A
    {
    };

    generic <class T1, class T2>
    where T1: A // okay, reference type
    where T2: int // struct from prohibited namespace
    public ref class Foo
    {
    };

  • Can specify only one reference type for each constraint (because multiple inheritance is not supported in .NET).


    public ref struct A
    {
    };

    public ref class B
    {
    };

    generic <class T1, class T2>
    where T1: A, B // error
    where T2: A, IComparable // ok
    public ref class Foo
    {
    };

  • The constraint clause can contain only one entry per each parameter.


    generic <class T>
    where T: IShape
    where T: IComparable // error C3233: ‘T’ : generic
    // type parameter already constrained
    ref class Foo
    {
    };

  • Multiple constraints for a parameter type are separated by a comma.


    generic <class T>
    where T: IShape, IComparable
    ref class Foo
    {
    };

  • Constraint type must be at least as accessible as the generic type of function.


    ref struct A
    {};

    generic <class T>
    where T: A // wrong, A is private
    public ref class Foo
    {};

  • Entries can follow any order, no matter the order of type parameters:


    generic <class T1, class T2, class T3>
    where T2: IComparable
    where T1: ISerializable
    where T3: IClonable
    public ref class Foo
    {
    };

  • Generic types can be used for constraint types too:


    generic <class T>
    public ref class A
    {};

    generic <class T1, class T2>
    where T1: A<T2>
    where T2: A<T1>
    public ref class Foo
    {};

Conclusions

This article is a brief introduction to the .NET generics with C++/CLI. Further readings are recommended to achieve a comprehensive view on generics (issues such as template specialization or invoking generic functions were not addressed, and may follow in an upcoming article). Though at a first glance templates and generics look the same, they actually are not. Templates are instantiated at compile time, generics at runtime. Templates support non-type parameters, default, values and template parameters, while generics do not. And although templates do not have any mechanism to explicitly specify constraints, generics provide a syntax for static verification, because instantiation is done at runtime, when is too late to discover than a type cannot be bound.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read