CROSS-PLATFORM DEVELOPMENT


Templates and Today's Compilers

Anil Admal and Chris Tarr

If you're going to use templates with today's technology, take advantage of the bleeding edge experience of others.


Copyright 1996, ObjectSpace, Inc. All Rights Reserved.

Introduction

Over the past couple of years we have created a variety of code requiring non-intrusive generalized techniques and the extensive use of C++ templates. During code creation, porting, and maintenance, we uncovered a number of compiler bugs related to use of templates. We pass this information along to you for use in your own template coding.

As we created this article and began the systematic description of common compiler bugs, we asked ourselves, "how will the reader benefit from this information?" This depends upon the reader of course. If you are coding intricate template code, this article will serve as a reference guide to template bugs common in today's compilers. If you are just learning templates, or have not used them extensively, this article presents a stimulating challenge in the semantics of C++. Simply read the code fragments and test you ability to follow the more powerful and challenging aspects of C++. Finally if you are a skimmer, and want simply to cut to the chase, skip to the summary of implications at the end of this article, and store them away for your future consideration.

Following are descriptions and examples of 23 different compiler problems related to the use of template structures, classes, and functions. The combination of problems varies from compiler to compiler, and generally increases the older the compiler is. No compiler, however, is free of all problems. At the end of this article is a matrix showing the problems found in ten of today's C++ compilers.

This article does not describe specific problems that occur when features are used in certain combinations in a certain context with certain compiler options, etc. These kinds of problems are generally specific to a single compiler and are tough to reproduce with a simple example. In addition, this article does not explore the bleeding edge issues of template member functions. None of the ten compilers chosen for this article support them.

The problems presented are categorized as follows:

All examples shown below use the keyword struct rather than class to eliminate the need to explicitly declare public access in each code fragment. These problems exist independent of whether you use struct or class.

Templates and Arguments

In this section we look at three problems that can result from using templated types as arguments in C++.

The first problem arises when a member function in a template struct takes another template struct as an argument. If the argument is given a default value, this can result in the Default arguments too complicated problem on some compilers.

Almost all compilers compile struct X below. However, some compilers have problems with struct Y, which uses X in the definition of a default argument to its constructor:

template< class T >
struct X
  {
  X( T t = T() ) {}
  };

template< class T >
struct Y
  {
  Y( const X< T >& x = X< T >() );
  };

Y< int > y1;

The workaround is to replace a function with default arguments with several functions without default arguments as shown below:

template< class T >
struct Y
  {
  Y();
  Y( const X< T >& x );
  };

Default arguments for template functions are not supported at all on some compilers. The following code shows the No default arguments for template functions problem. In this code fragment we define a template function foo that takes two parameters of type T, and defaults the second one to an instance of T constructed with no arguments. On some compilers you are simply told this is not supported:

template< class T >
void
foo( T t1, T t2 = T() )
  {
  }

foo(2);

In the next code fragment, we declare a template struct X with two member functions foo1 and foo2. The function foo1 explicitly states that the argument is of type X< T >, repeating the template signature. Function foo2 also takes a single X< T > argument, however the template signature is reduced to X. This shorthand is allowed inside the scope of struct X, and means the same as X< T > based on the latest ANSI/ISO draft. Older versions of draft and the C++ Annotated Reference Manual (ARM) were not clear regarding this shorthand notation, and therefore some compilers do not compile foo2. These compilers Require template types on every type usage.

template< class T >
struct X
  {
  void foo1( X< T >& );
  void foo2( X& );
  };

Most of the problems with templates and arguments are easy to work around if you follow these simple guidelines.

Templates and Constructors

Three problems arise concerning the use of constructors with template code:

To illustrate these problems, let's declare a template struct Y with a single data member my_data. A single constructor is declared that takes no arguments and calls the default constructor for my_data.

Template< class T >
struct Y
  {
  Y() : my_data() {}

  T my_data;
  };

Now declare a non-template struct X with no user-defined constructors. The compiler in this case generates a default constructor for you. Next instantiate the template struct Y using X. On some compilers, this case results in the No call of compiler default constructor problem. On these compilers you cannot explicitly call compiler-generated default constructors in the initialization list for a template constructor.

struct X
  {
  };

Y< X > object;

Some compilers choke if you explicitly ask them to call constructors to pointers, resulting in the No call of pointer constructor problem. This can be caused by instantiating Y on a pointer.

Y< int* > object;

The final problem occurs when you explicitly call a constructor on a constant object, resulting in the No call of constructor on const types problem. By instantiating Y with a const class, this problem can result.

Y< const X > object;

A possible workaround for this problem is to rewrite the constructor initialization list to pass a temporary object into the constructor of the const type as illustrated below.

Y() : my_data( T() ) {}

This may slow down execution, but is a viable workaround on some compilers.

To avoid the constructor problems that can occur when using templates, follow these guidelines.

Templates and Destructors

Destructors suffer a similar plague of problems as constructors. In most cases you will not encounter these unless you explicitly call a destructor. This is a rare programming technique used only in the most advanced development efforts. It can result in the following three problems:

The following code fragment defines a struct X with a simple destructor, and a template function destroy which explicitly calls the destructor for its argument. On some compilers, calling destroy with the pointer to an instance of X results in the No explicit call of template destructor problem.

struct X
  {
  ~X() {}
  }

template< class T >
void destroy( T* ptr )
  {
  ptr->~T();
  }

X object;
destroy( &object );

Some compilers can handle this case, but cannot explicitly call compiler generated destructors (No explicit call of compiler destructor). Finally, other compilers cannot handle explicit calls to destructors on primitive types (No explicit call of primitive destructor).

To help avoid these problems in your own code or when using other people's templates, always explicitly define a destructor for a class or struct.

Templates and Inheritance

Two general problems arise when using templates and inheritance:

Some compilers do not allow virtual functions to be declared outside the body of a class or struct. This is the No non-inline virtual functions problem.

The following code fragment declares a template struct X with two member functions. The function foo1 is defined inside the body of struct X, and will compile on most compilers. The function foo2 is defined outside struct X and might not compile.

template< class T >
struct X
  {
  virtual void foo1() {}
  virtual void foo2();
  };

template< class T >
void X< T >::foo2()
  {
  };

X< int > object;

The next code fragment is an illustration of the No template base class conversion problem. This severe problem results in the failure of compilers to implicitly convert a class reference into a reference to its base class. This cripples the ability to write polymorphic code.

Below we define a template struct X. Then we define a template struct Y that inherits X. The template function foo is defined to take a reference to the struct X. We then construct a concrete instance of Y< int > and call foo. Some compilers fail to implicitly convert a reference to the struct Y into a reference to its base class struct X. Instead they report an error. Except for manual casting to the base type, there is no known workaround for this problem.

template < class T >
struct X {};

template < class T >
class Y : public X< T > {};

template < class T >
int foo( X< T >& )
  {
  return 0;
  }

Y< int > y;
foo( y );

To be safe, define virtual functions inside the body of a class or struct.

Templates and Linking

Incorrectly instantiating templates and linking them together is the cause of most problems when people first use templates. An entire article could be written on the topic of template closure and linking. Here we discuss just a few problems related to linking templates.

Some compilers simply do not support static data in template classes or structs. These compilers will fail to compile the basic code fragment below, where we declare a template struct X with a single static data member i. On some compilers this will not link, resulting in the Failure to link template static data problem.

template < class T >
struct X
  {
  static int i;
  };
        
template < class T >
int X< T >::i = 0;

int main ()
  {
  X< float > x;
  X< float >::i = 0;
  return 0;
  }

Each compiler instantiates templates differently. Some compilers use a common repository to store template instantiations. Other compilers instantiate a separate copy of a template in each compilation unit that uses it. These compilers often have problems with static data in functions, resulting in the Duplicate template function statics problem.

Below we declare a template function foo with a static data member my_data. If foo is used in two different source files, the compiler may create a separate copy of my_data in each compilation unit in which it is used.

template< class T >
T& foo( T* )
  {
  static T my_data;
  return my_data;
  }

// Source file 1 - a.cpp
int& i1 = foo( (int*) 0 );

// Source file 2 - b.cpp
int& i2 = foo( (int*) 0 );

The following severe problem, Failure to instantiate global template functions, is particularly troublesome. This problem occurs when template functions are not defined as inline. Unfortunately, some code cannot be inlined on some compilers when it uses loop constructs such as while. Therefore the user hits a wall. The template function will not instantiate if non-inline, and will not compile if it is inline.

In the following example, the non-inline global function foo is defined in f.h. A single struct X is defined in main.cpp. If an instance of foo is used in main.cpp the compiler may not properly instantiate foo( X ) and link it.

// f.h
template< class T >
int foo( T& t )
  {
  // non-inlineable code
  }

// main.cpp
struct X
  {
  X() {}
  };

int
main()
  {
  X x;
  return foo( x );
  }

The only known workaround for this problem is to replace the non-inline function by an inline template function which turns around and calls a static member function of a struct. Below we rewrite the function foo to use this workaround.

// f.h
template< class T >
struct f_aux
  {
  static int foo( T& t )
    {
    // non inlineable code.
    }
  };

template< class T >
inline int foo( T& t )
  {
  return f_aux< T >::foo( t );
  }

To minimize the types of linking problems shown here follow these guidelines.

Templates and Typedef Access

In advanced template programming, the use of typedefs can allow programmers to pass additional types as part of a single template parameter. The following examples illustrate this technique, which unfortunately cannot be handled by many compilers. Two cases are presented here, each of increasing complexity. Each case results in a possible compiler problem.

In the first case, No simple class typedef in other typedef, we declare a simple struct A which contains a simple typedef for type1. Then we declare a template struct X which defines a second typedef type2 to be T::type1. This technique allows us to associate a second type int with the type A inside struct X even though struct X has only a single template parameter. This technically works well when there are fixed type associations. Unfortunately, even this simple case will not compile on some compilers.

struct A
  {
  typedef int type1;
  };

template< class T >
struct X
  {
  typedef T::type1 type2;
  }

X< A > object;

Note that in these examples the typename keyword is not used at the appropriate places for simplicity.

Some compilers handle the simple case, but then fail as soon as struct A is templatized (No template class typedef in other typedef). Let's change the definition of A as follows:

template< class T >
struct A
  {
  typedef T type1;
  };

Now instantiate X using the new template based A and this will break some more compilers.

X< A< int > > object;

The only way to work around all template access problems is to use additional template parameters. Some simple cases will not require this, and can be avoided by replicating typedefs rather than accessing them from other classes.

Templates and Basic Typedef Usage

Even the simple cases of template typedefs can cause a couple of problems when used as basic return types or function arguments.

Some compilers do not allow typedefs to be used as return types when member functions are defined outside the body of the structure or class. In the following example we define a template struct X which includes a simple typedef type1. The first function foo1 is defined in the body of the struct X. Most compilers can compile foo1. The function foo2, however, is defined outside the body of the struct X, which results in the No template class typedef as return value problem.

template< class T >
struct X
  {
  typedef T type1;
  type1 foo1()
    {
    type1 t;
    return t;
    }

  type1 foo2();
  }

template< class T >
X< T >::type1 X< T >::foo2()
  {
  type1 t;
  return t;
  }

Another problem, similar to the typedef access problems, arises when you use another struct's typedef as an argument to a member function. Below we declare another template struct Y. The function foo in Y has a single argument defined using the typedef from struct X above. On some compilers this results in the No other template class typedef as function argument problem.

template< class T >
struct Y
  {
  void f1( X< T >::type1 );
  };

Y< X< int > > y;

Minimize basic typedef usage problems by following these simple rules.

Template and Typedefs with Inheritance

Another category of typedef problems results when you use inheritance with typedefs. Two problems are described here.

Both of these problems will be shown with the same code fragments below. Start with a simple struct A containing a simple typedef type1. Next define a template struct B with a typedef type2.

struct A
  {
  typedef int type1;
  }

template< class T >
struct B
  {
  typedef T type2;
  }

Now we uncover both problems with a derived template struct C, instantiated using both A and B.

template< class T >
struct C : B< T::type1 >
  {
  void foo( type2 arg );
  }

C< B< A > > object;

The struct C inherits from a struct B instantiated on a typedef from A. The use of a typedef from another class or struct as a template parameter during inheritance results in the No typedefs as template arguments in inheritance problem on some compilers.

If your compiler gets past the inheritance, it may still fail to see the inherited typedef type2. Because type2 is public in the base struct B, it should be visible from C. Some compilers, however, do not see this and claim type2 to be undefined.

Avoid typedef inheritance problems by following these basic rules:

Templates and Type Conversion

Several compilers have problems performing correct type conversions or correctly managing const as a type modifier. This results in three main problems.

The following code illustrates the No const conversion problem. This problem results in compilers failing to perform implicit const conversions. At the end of the code fragment below, the template function foo is invoked with an instance Y< const X > where Y is a simple template structure and X is any other class or structure. The function foo takes a const Y< T > as an argument. When an exact match of

const Y< T > cannot be made, the function foo should also match const Y< const T >. Unfortunately, some compilers will fail to implicitly perform const conversion of the template argument during signature matching and will fail to find a match for the call to foo below.

struct X {};
template< class T >
struct Y {};

template< class T >
void foo ( const Y< T >& ) {}

Y< const X > p1;
foo( p1 );

Some compilers fail const conversion even if X is a primitive type. Others correctly handle const conversion of primitives, but still fail the above example when X is a struct or class.

The following code illustrates the Failure to differentiate const problem. Here we define three insert functions within the template struct Y. Each function technically has a different signature because of the differing use of const. Some compilers consider the three insert functions to be identical, and fail to differentiate based upon const template parameters in arguments. In this example you may get a multiply defined error or an ambiguous error upon calling insert().

template< class T1, class T2 >
struct X {};

template< class T1, class T2 >
struct Y
  {
  void insert
    (
    const X< const T1, T2 >&
    ) {}
  void insert
    (
    const X< T1, const T2 >&
    ) {}
  void insert
    (
    const X< const T1, const T2 >&
      ) {}
  };

X< const int, int > x;
Y< int, int > y;
y.insert( x );

The use of overloaded function signatures is one of the key powers of C++. Unfortunately many compiler implementations do not correctly handle overloaded template functions.

The following code fragment illustrates the Failure to select specialized template function problem. Two global template print functions are defined. One takes a T argument and the other takes an X< T > argument. When print is invoked with an instance of X< int >, some compilers correctly match print( X< T > ). Some compilers, however, declare the print functions to be ambiguous.

template < class T >
struct X {};

template< class T >
void print( T t ) {}

template< class T >
void print( X< T > t ) {}

X< int > x;
print( x );

Your best bet in the short run is to avoid the use of const in the specification of template parameters. Since some compilers fail to correctly perform const conversion, and others fail to differentiate const signature differences, it is difficult to work around these problems. In addition, avoid overloading template functions where the only differences are specialized parameters.

Compiler Test Results

We tested ten of today's compilers for the problems previously described. The results of our testing are in matrix shown in Figure 1. New compiler releases or compiler patches may affect the results of tests. Be warned though, problems that seem to be fixed can sometimes reappear in future releases.

Summary of Implications

If you are writing code for one or two compilers only, you will quickly find the mix of workarounds and constraints that will allow you to write portable code. The matrix of test results in the previous section will serve as a starting guide to help you locate potential problems.

If you are writing code that must be portable to more that one or two compilers, the set of restrictions will be more difficult. Here is a summary of the main guidelines presented in this article. Following these guidelines will help eliminate some, but not all template problems you may encounter.

Compilers are improving all the time. The C++ community can expect a wave of next-generation ANSI/ISO-compliant compilers during the coming year. Unfortunately, that means it will be two to three years before programmers and their customers will phase out older compilers to the extent required to eliminate all of the problems discussed in this article.

As you move your code to the newer ANSI/ISO compliant compilers, keep these parting suggestions in mind:

We have found the use of templates improves efficiency and reuse in many applications. There are several pitfalls, however, as is true of many C++ constructs. Hopefully the suggestions in this article will help save you debug time and help to improve your own group's coding standards. We look forward to the day when the bulk of compilers pass the tests. Until that time, we all will need to exercise a little restraint and make an occasional workaround from time to time. o

Anil Admal is an Object Technologist at ObjectSpace, Inc. in Dallas, TX. He is a member of the development team working on the ObjectSpace Systems<ToolKit> C++class library. He can be reached at aadmal@objectspace.com.

Chris Tarr is Vice President of Software Component Technology at ObjectSpace, Inc. in Dallas, TX. He manages a business unit dedicated to the development, marketing, and sale of object oriented software components. He can be reached at ctarr@objectspace.com.