Pure C++: Reflecting on Generic Types

We were unable to locate this content in de-de.

Here is the same content in en-us.

Pure C++
Reflecting on Generic Types
Stanley B. Lippman

A funny thing happened to templates on their way to the common language runtime (CLR)—they lost their {type} identity. This is analogous to what happens with macros under native programs. Just as the C/C++ compilers have no awareness of macro preprocessor expansions, the CLR has no awareness of template instantiations. In both cases, the expansions are baked into the data stream being processed, and all unique {type} identity is lost. (This has some design ramifications that I will address in a subsequent column when I look at STL/CLR, the Standard Template Library rearchitected for Visual C++® 2005.)
Generics, on the other hand, are directly supported by the runtime. Extensions to the Common Intermediate Language (CIL) explicitly support the specification of a generic type, as well as its type parameters, constraints, and so on. And an extension to the Reflection type facilities of the Base Class Libraries of the System namespace allow full reflection capabilities on generic types and object instances. Moreover, the runtime automatically handles the instantiation of generic instances in an optimal way. In this column, I provide an introduction to reflection and support for generic reflection in the .NET Framework 2.0.

Type Information in .NET
Most compiler writers, at some point in their careers, come to the realization that a good part of each day is spent figuring out how to be smarter about translating a program, building up a rich in-core program representation to support those smarts, and finally tossing all that information away when finished. Under .NET, however, this in-core program representation is persisted in the form of a standardized metadata specification, and is available both to the runtime (which has full access to the CIL and the generated metadata) and to you at execution time through run time discovery, known as reflection.
For example, when you write the following:
enum class test { fail, succeed, untested, rerun };
the underlying System::Enum class provides static methods through which you can access the enumerator names, their associated values, and so on. In order to print out the enumerator names by ascending order of the corresponding integral value, you can write this:
for each( String ^s in Enum::GetNames( 
   status::typeid ))
Console::WriteLine( s );
It is worth taking a moment to think about what is happening here with the invocation of Enum::GetNames, rather than just jotting down how it's done. What you want to retrieve is the name of the enumerations of your enum status. It would not make practical sense to associate those names with an instance of status, which only needs to hold an integral value. Rather, the names and values of the enumerators are invariant and pertain to all instances of the enum you have defined. This represents a form of metadata. You only need to store it once, and the class through which this metadata is stored is System::Type—the System::Type that is associated with your enum status.
There are a number of ways of accessing the Type object associated with a type. T::typeid is the C++/CLI extension of the native RTTI (C++ Run-Time Type Information) typeid operator that returns the Type object associated with the specified type T—in this case, the status enum. This use of metadata through run-time access of Type objects is called, as I mentioned before, reflection.
If you have an object of some type, its associated Type is retrieved through the GetType instance method. For example, here is a general method to retrieve an object's associated Type and display both its type name and fully qualified type name:
void DisplayType( Object^ o )
{ 
    if ( !o ) return;

    Type^ t = o->GetType(); 
    Console::WriteLine( "Type is {0} : {1}", 
                         t->Name, t->FullName ); 
}
Figure 1 shows code that passes various types to the DisplayType method, including a class generic type. This generates the output shown in Figure 2. (Note that I've cleaned up the line-spill a bit and annotated the results with comments. Also remember that the output may not be exactly as will be seen with the final product release.) Notice that the fully qualified name for a Type displays the scope operator as dot (.), the intermediate language (IL) scope operator, rather than as the C++ double-colon (::). Also take note of the full system information available for the generic type.

// Int32 i1 = 1024; DisplayType( i1 );
Type is Int32 : System.Int32
// String^ s1 = "Foo"; DisplayType( s1 );
Type is String : System.String
// array<bool>^ bits; DisplayType( bits );
Type is Boolean[] : System.Boolean[]
// DisplayType( gcnew Container<int> );
Type is Container`1 : templates.Container`1[[System.Int32, mscorlib, 
Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
// DisplayType( gcnew Container<String^> );
Type is Container`1 : templates.Container`1[[System.String, mscorlib, 
Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
namespace foobar
{
    generic <class T> ref class Container{};
}

using namespace forbar;
int main()
{
    Int32 i1 = 1024;
    String^ s1 = "Foo";
    array<bool>^ bits = gcnew array<bool>( i1 );

    DisplayType( i1 );
    DisplayType( s1 );
    DisplayType( bits );

    DisplayType( gcnew Container<int> );
    DisplayType( gcnew Container<String^> );
}
If you do not have an object through which to grab the associated Type object, you can explicitly retrieve it using the typeid operator as follows:
Type^ ts = String::typeid; 
Type^ ti = Int32::typeid;
Of course, if you have not specified a using statement for the relevant namespace, you will need to qualify the name of a type located in that namespace. For example:
Type^ tsb = System::Text::StringBuilder::typeid;
If you are unsure if the actual type exists, then you cannot actually use the explicit typeid operator. Rather, you can use the static GetType method of the Type class by passing it a string literal of the type name. You might do this, for example, to read a file or collection of type names that may or may not exist within your application (see Figure 3).
void DisplayType( String^ typeName )
{ 
    Type^ t = Type::GetType( typeName ); 
    
    // if null, couldn't find it...
    if ( ! t ) return;

    Console::WriteLine( "Type is {0} : {1}", 
                         t->Name, t->FullName );
}

void display( array<String^>^ types )
{ 
    for each ( String^ ts in types )
        DisplayType( ts );
}

List<Type^>^ 
fetchTypes( array<String^> ^args)
{
    List<Type^> ^actualTypes = gcnew List<Type^>;

    for each ( String^ to in args )
          if ( Type^ tt = Type::GetType(ts) )
               actualTypes->Add( tt );

    return actualTypes;
}
There are a number of possible forms your literal type name can take. If you provide an unqualified name, such as the following, the type is found only if it is located at global scope:
// Unqualified name: not found if we intend System::Math ...
DisplayType( "Math" );
When you specify a fully qualified name, you must use the IL scope operator rather than that of C++. For example:
// A fully qualified name, but we can't use :: 
DisplayType( "System.Math" );
Note that you cannot use this form of retrieval for a class generic or class template type. This is because the template or generic definition does not represent an actual type but only a pattern for generating instances based on actual type arguments.
A third form of string literal to retrieve a type name is to both fully qualify the name and specify the assembly within which you believe it will be found. The assembly is separated from the type by a comma, as in the following:
// A fully qualified name, and assembly 
DisplayType( "System.Math, mscorlib" );
If you have an Assembly object, you can either retrieve a Type array of all the types defined within that assembly, or query for a particular type, as shown here:
array<Type^>^ types = a1->GetTypes();
Type^ query = a1->GetType( "Query" );
What if you have a Type, and want to discover all the other types defined within its assembly? Or, given an object, you want to discover which assembly its type definition is defined in? Or what if you want to know a half dozen other things about the type: is it an array, is it a class, is it a generic type, and so on? You can query and retrieve almost all of this information from a Type object.
The Type class is actually something magical; it serves as the key that unlocks the runtime reflection facility. For example, Figure 4 is a simple run-time utility that either returns the name of the type's base class, if it has one, or returns nullptr.
ref class TypeUtilities
{
    Assembly^ m_a;
    ...

public:

    String^ baseClass( String ^typeName )
    {
        Type^ query = m_a->GetType( typeName );
        if ( query && query->IsClass )
        {
            if ( Type^ base = query->BaseType )
                 return base->Name;
        } 
             
        return nullptr;
    }
}
A Type supports a number of queries, such as whether the type is a class (IsClass). It returns additional type information, such as the Type of its base class, or if it should have one (BaseType). It also provides general properties of the type, such as its name (Name,FullName). I've categorized the services provided by Type into the general designations, which I'll discuss next.

IsA/HasA Queries
The IsA/HasA queries tell you whether the type is this thing or that. I used IsClass already. Other IsA properties include IsAbstract, IsArray, IsNested, IsPublic, IsSealed, IsSerializable, IsValueType, and so on. The HasA properties include HasGenericArguments, which returns true if the type has type arguments, and HasElementType, which returns true if the type holds or refers to another type—that is, if it is an array, a pointer, or is passed by reference. Figure 5 shows how you might discover a generic type at run time.
void DisplayGeneric( Type^ t )
{
    if ( !t ) return;
    String^ name = t->Name;
    String^ display1 = "{0} is {1} a generic type definition!";
    String^ display2 = "{0} is {1} a generic instance!";

    if ( ! t->IsGenericTypeDefinition )
         Console::WriteLine( display1, name, " not" );
    else Console::WriteLine( display1, name,"" );

    if ( ! t->HasGenericArguments )
         Console::WriteLine( display2, name, " not" );
    else Console::WriteLine( display2, name, "" );
}
IsGenericTypeDefinition Apart from being impossible to type correctly, this returns true if the Type represents a generic definition. It returns false on an instance of the generic type, as well as on any non-generic type, including that of a template. For example, Figure 6 shows three types passed in and their associated output.
//    class: not a generic type definition
String^ bs = "not generic";
DisplayGeneric( bs->GetType() );

String is not a generic type definition!

// class generic: not a generic type definition
Container<String^> ^cs = gcnew Container<String^>;
DisplayGeneric( cs->GetType() );

Container`1 is not a generic type definition!

// class generic definition: ok!
Type^ gd = cs->GetType()->GetGenericTypeDefinition();
DisplayGeneric( gd );

Container`1 is a generic type definition!
HasGenericArguments This returns true if the Type represents either a generic definition or an instance of a generic type. For example, here is the output from running the three same types against this HasA property:
String^ is not a generic instance!
Container`1 is a generic instance!
Container`1 is a generic instance!
Because HasGenericArguments returns true on a generic definition and a generic instance, if you just want to handle a generic instance, you need some sort of conditional test, like this one:
if ( ! t->IsGenericTypeDefinition &&
       t->HasGenericArguments )
     // ok, a generic instance ...
     
GetGenericTypeDefinition represents a second category of service: retrieving a special aspect of the type.

Get Retrievals
These retrieve some specific type or collection of types that are possibly associated with this Type object. GetGenericTypeDefinition is one such method. To illustrate some of these retrieval methods (highlighted in red type) that are associated with generic types, let's augment DisplayGeneric to probe more deeply (see Figure 7).
void DisplayGeneric( Type^ t )
{
    // initial portion remains unchanged...

    if ( ! t->IsGenericTypeDefinition )
         Console::WriteLine( display1, name, " not" );
    else 
    {  // let's probe the generic type definition ...

       // does it have type parameters ???
       array<Type^> ^parameters = t->GetGenericArguments();
       if ( parameters ){
          int count = parameters->Length;
          Console::WriteLine( "{0} has {1} parameters.",
                              name, count );

          for ( int ix = 0; ix < count; ++ix ){
              Type^ t = parameters[ ix ];
              Console::WriteLine( "\n\t{0} at position {1}", 
                         t->Name, t-> GenericParameterPosition ); 

              array<Type^> ^constraints = 
                    t-> GetGenericParameterConstraints();
              if ( constraints ){
                 int count = constraints->Length;
                 Console::Write("\t{0} has {1} constraint(s): ",
                            t->Name, count );
                 for ( int ix = 0; ix < count; ++ix ){
                      Type ^tt = constraints[ix];
                      Console::Write( " {0}", tt->Name );
                  }
                  Console::WriteLine();
              }
              Console::WriteLine();
           } // for each parameter
        } // if parameters
    } // else if generic definition
} // end of DisplayGeneric()...
GetGenericArguments returns an array of Type objects representing each of the generic type parameters. Each parameter element has an associated position (beginning at 0)—this is the return value of GenericParameterPosition. GetGenericParameterConstraints returns associated constraint clauses, if any, as an array of Type objects. Not being clear about any constraints on the use of this method, I mistakenly applied it to the generic definition itself rather than to the individual parameters. This results in a run-time exception:
System.InvalidOperationException: Method may only be called on a Type 
for which Type.IsGenericParameter is true.
In any case, I changed the types to pass into this revised method. In the first case, I added three constraints to our class generic, as shown in the following:
generic <class T>
    where T : IComparable<T>,
              System::Collections::IEnumerable, ICloneable
ref class Container{};
When I invoke the method like this
Container<String^> ^cs = gcnew Container<String^>;
Type^ gd = cs->GetType()->GetGenericTypeDefinition();
DisplayGeneric( gd );
it generates the following output:
Container`1 is a generic type definition!
Container`1 has 1 parameters.

        T at position 0
        T has 3 constraint(s):  IComparable`1 IEnumerable ICloneable
The second class generic is an instance of the SortedDictionary container located in the System::Collections::Generic namespace. Here is what the call looks like:
SortedDictionary<String^, String^>^ gds = 
      gcnew SortedDictionary<String^, String^>;
Type^ gdst = gds->GetType()->GetGenericTypeDefinition();
DisplayGeneric( gdst );
And here is the somewhat surprising output (I had expected a larger, more specific constraint set):
SortedDictionary`2 is a generic type definition!
SortedDictionary`2 has 2 parameters.

        K at position 0
        K has 1 constraint(s) :  Object

        V at position 1
        V has 1 constraint(s) :  Object
As I mentioned at the outset of this column, the next (and final) column on parameterized types under Visual C++ 2005 will look at the design interoperability of templates and generics using our STL/CLR library as a model. Until then, may your programs execute without bugs.

Send your questions and comments for Stanley to  purecpp@microsoft.com.


Stanley B. Lippman is an Architect with the Visual C++ team at Microsoft. He began working on C++ with its inventor, Bjarne Stroustrup, in 1984 at Bell Laboratories. In between, he worked in feature animation at Disney and DreamWorks, was a Distinguished Consultant with JPL, and was a Software Technical Director on Fantasia 2000.

Page view tracker