Using Template Functions to Customize Library Behavior

If you're writing code to be used by other programmers, you'll want to make it customizable, and you'll want to do it well. This article presents an approach that is both efficient and easy to manage.


March 01, 2001
URL:http://www.drdobbs.com/using-template-functions-to-customize-li/184401370

Michael B. Yoder is a software engineer at Rogue Wave Software. For the past two and a half years he has worked as a part of the Foundation Group in teams responsible for Rogue Wave’s Tools.h++ Library and C++ Standard Library implementation. He has a B.S. in Computer Science from Oregon State University.

March 2001/Using Template Functions to Customize Library Behavior/Listing 1

Listing 1: Providing a user-definable policy via a macro

//
//---- begin file library.h ----
//

#ifndef CLASS_POLICY
#  define CLASS_POLICY "default policy"
#endif                  

//
//---- begin file container.h ----
//

#include "library.h"
#include <string>
#include <iostream>


template <class ElementType>
class MyContainer {
public:
   MyContainer(const std::string iName) : instanceName(iName) 
   {}
   // class implementation
   // ...
   
   // performs the policy
   void doPolicy();
   // a service that uses the policy
   void performService() { doPolicy(); }
   
   const std::string instanceName;
};

// elements for example's sake
template<class T>
struct Element1 {
// ...
};

template<class T>
struct Element2 {
// ...
};


template <class ElementType>
void MyContainer<ElementType>::doPolicy() 
{ 
    std::cout << "Policy is: " << CLASS_POLICY << std::endl;
}


//
//---- begin file user.cpp ----
//

#include "container.h"

int main() {
    
   MyContainer<Element1<int> > c("object1");
   c.performService();

   return 0;
}
— End of Listing —
March 2001/Using Template Functions to Customize Library Behavior/Listing 2

Listing 2: Providing a user-definable policy via a map of function pointers

//
//---- begin file library.h ----
//

#include <string>
#include <map>
#include <iostream>

typedef void(*policyFunction)();
typedef std::map<std::string, policyFunction> policyMap_t;

// singleton class to manage policies
class Policies {
public:
   void registerPolicy(const std::string id,
                       const policyFunction func);
   void policy(const std::string id) const;

   static Policies& policyMap();
private:
   Policies() {}
   policyMap_t typeMap_;
};

// member function definitions
void
Policies::registerPolicy(const std::string id,
                         const policyFunction func)
{
   if (id != "" && func !=0) {
      typeMap_[id] = func;
   }
}

void Policies::policy(const std::string id) const
{
   policyMap_t::const_iterator iter = typeMap_.find(id);
   if (iter != typeMap_.end()) {
      (*iter).second();
   }
   else {
       std::cout << "Performing Default Policy." 
                 << std::endl << std::endl;
   }
}
    
Policies& Policies::policyMap() 
{
   static Policies typeMap;
   return typeMap;
}    

// namespace scope function for convenience
Policies& policyMap()
{
   return Policies::policyMap();
}

//
//---- begin file container.h ----
//

#include "library.h"
#include <string>
#include <iostream>

template <class ElementType>
class MyContainer {
public:
    MyContainer(const std::string iName) : instanceName(iName) 
   {}
   // class implementation
   // ...
   
   // forwards the policy request
   void doPolicy();
   // a service that uses the policy
   void performService() { doPolicy(); }
   
   const std::string instanceName;
};

// elements for example's sake
template<class T>
struct Element1 {
// ...

};

template<class T>
struct Element2 {
// ...
};


template <class ElementType>
void MyContainer<ElementType>::doPolicy() 
{ 
    policyMap().policy("MyContainer");
}

//
//---- begin file user.cpp ----
//

#include "container.h"

void MyPolicy()
{
    std::cout << "Performing My Policy." 
              << std::endl << std::endl;
}
           
           
int main() {

   MyContainer<Element1<int> > c("test object");
   c.doPolicy();
   policyMap().registerPolicy("MyContainer", MyPolicy);
   c.doPolicy();

   return 0;
}
— End of Listing —
March 2001/Using Template Functions to Customize Library Behavior/Listing 3

Listing 3: Providing a user-definable policy via specialization of a function template

//
//---- begin file library.h ----
//

#include <iostream>

template<class Container>
void policy(const Container * c) 
{
    std::cout << "Performing Default Policy" 
              << std::endl << std::endl;
}    

//
//---- begin file container.h ----
//

#include "library.h"
#include <string>
#include <iostream>

template <class ElementType>
class MyContainer {
public:
    MyContainer(const std::string iName) : instanceName(iName) 
   {}
   // class implementation
   // ...
   
   // forwards the policy request
   void doPolicy();
   // a service that uses the policy
   void performService() { doPolicy(); }
   
   const std::string instanceName;
};

// elements for example's sake
template<class T>
struct Element1 {
// ...
};

template<class T>
struct Element2 {
// ...
};

template <class ElementType>
void MyContainer<ElementType>::doPolicy() 
{ 
   policy(this);
}

//
//---- begin file user.cpp ----
//

#include "container.h"

template<class T>
void policy(const MyContainer<Element2<T> > * c) 
{
    std::cout << "Performing My Policy" 
              << std::endl << std::endl;
} 
           
int main() {

   MyContainer<Element1<int> >  c("object1");
   MyContainer<Element2<int> >  d("object2");
   c.doPolicy();
   d.doPolicy();

   return 0;
}
— End of Listing —
March 2001/Using Template Functions to Customize Library Behavior/Listing 4

Listing 4: Combining run-time and compile-time policy customization methods of Listings 2 and 3

//
//---- begin file library.h ----
//

#include <map>
#include <string>
#include <iostream>

typedef void(*policyFunction)();
typedef std::map<const void *, policyFunction> policyMap_t;

// singleton class to manage policies
class Policies {
public:
   void   registerInstance(const void * const v, 
                           const policyFunction func);
   policyFunction isRegistered(const void * const v) const;
   void   policy(const void * const v) const;
   static Policies& instancePolicyMap();
private:
   Policies() {}
   policyMap_t instanceMap_;
};

// member function definitions
void
Policies::registerInstance(const void * const v, 
                           const policyFunction func)
{
   if(v != 0 && func != 0) {
      instanceMap_[v] = func;
   }
}

policyFunction
Policies::isRegistered(const void * const v) const 
{
   policyMap_t::const_iterator iter = instanceMap_.find(v);
   if (iter != instanceMap_.end()) {
      return (*iter).second;
   }
   else {
      return 0;
   }
}

void
Policies::policy(const void * const v) const
{
   if (policyFunction pFunc = isRegistered(v)) {
      pFunc();
   } 
}

Policies&
Policies::instancePolicyMap()
{
   static Policies typeMap;
   return typeMap;
}

// namespace scope function for convenience
Policies& instancePolicyMap() 
{
   return Policies::instancePolicyMap();
}    


template<class Container>
void policy(const Container * const c) 
{
   std::cout << "Performing Compile Time Default Policy" 
             << std::endl;
   std::string obj;
   if (c) 
      obj = c->instanceName;
   else
      obj = "unnamed";
   std::cout << "    on Object: " << obj 
             << std::endl << std::endl;
}    
    
//
//---- begin file container.h ----
//

#include "library.h"
#include <string>
#include <iostream>

template <class ElementType>
class MyContainer {
public:
    MyContainer(const std::string iName) : instanceName(iName) 
    {}
    // class implementation
    // ...
    
    // forwards the policy request
    void doPolicy();
    // a service that uses the policy
    void performService() { doPolicy(); }
    
    const std::string instanceName;
};

// elements for example's sake
template<class T>
struct Element1 {
// ...
};

template<class T>
struct Element2 {
// ...
};

template <class ElementType>
void MyContainer<ElementType>::doPolicy() 
{ 
    policy(this);
}

//
//---- begin file user.cpp ----
//

#include "container.h"


// policy functions used for runtime instance control
void runTimePolicy1()
{
    std::cout << "    runTimePolicy1() executed" << std::endl;
}

void runTimePolicy2()
{
    std::cout << "    runTimePolicy2() executed" << std::endl;
}

void runTimePolicy3()
{
    std::cout << "    runTimePolicy3() executed" << std::endl;
}

// A more specialized function overload for a compile time policy
template<class T>
void policy(const MyContainer<T> * c) 
{
   std::cout 
      << "Performing My Compile Time Policy for "
      << "types of:" << std::endl << "MyContainer<Element1<T>"
      << std::endl;
   std::string obj;
   if (c) 
      obj = c->instanceName;
   else
      obj = "unnamed";
   std::cout << "    on Object: " << obj 
             << std::endl << std::endl;
} 

// A more specialized function overload for instance control
template<class T>
void policy(const MyContainer<Element2<T> > * const c)
{
   std::cout << "Performing Runtime Policy: " 
             << std::endl;

   if (!instancePolicyMap().isRegistered(c)) {
       
      // a per instance strategy would go here
      static unsigned int r = 0;
      switch(++r % 3) {
          case 1:
             instancePolicyMap().
                registerInstance(c, &runTimePolicy1);
             break;
          case 2:
             instancePolicyMap().
                registerInstance(c, &runTimePolicy2);
             break;
          case 0:
          default:
             instancePolicyMap().
                registerInstance(c, &runTimePolicy3);
      }
    }       

   std::string obj;
   if (c) 
       obj = c->instanceName;
   else
       obj = "unnamed";
   std::cout << "    Object: " << obj << std::endl;

   instancePolicyMap().policy(c);
} 
               
           
int main() {

   MyContainer<Element1<int> > 
       c("MyContainer<Element1<int> >");
   MyContainer<Element2<float> > 
       d1("MyContainer<Element2<float> > Instance#1");
   MyContainer<Element2<float> > 
       d2("MyContainer<Element2<float> > Instance#2");
   MyContainer<Element2<float> > 
       d3("MyContainer<Element2<float> > Instance#3");

   c.doPolicy();
   d1.doPolicy();
   d2.doPolicy();
   d3.doPolicy();

   return 0;
}
— End of Listing —
March 2001/Using Template Functions to Customize Library Behavior/Sidebar

Understanding Function Template Selection


In C++, a more specialized version of a class template is called a partial specialization. While the syntax for specialized function templates is the same as for partial specializations, the behavior can be somewhat different. Hence, the term "partial specialization" is not used with regard to functions. While class templates are partially specialized, functions are simply overloaded. The function template overload a compiler chooses in response to a particular call depends on the concept of which one is "more specialized" than the others. The process is similar to selecting one candidate over the other if and only if the set of all functions possibly generated from the selectee is a proper subset of another.

For example, consider these two function templates:

template<class T>
void foo(T t);
 
template<class T>
void foo(std::vector<T> t);

Of these two, the second would be considered more specialized than the first. When considering the first function template, the compiler could generate a function with distinct types starting out like this:

void foo(int t);
void foo(float t);
void foo(std::string t);
...

For the second, the compiler could generate:

void foo(std::vector<int> t);
void foo(std::vector<float> t);
void foo(std::vector<std::string> t);
...

Both of these lists could go on forever; they are unbounded sets. In the first list, the parameter can be any distinct type, but in the second, the parameter must be the vector class specialized on a type. Any element in the second list could be used in the first, while the reverse is not true. This makes the second list a proper subset of the first.

Using function templates as described here is intuitive and useful. The subset discussion is a simplification, however, since the criterion for whether a function generated by one template function could be used by another is whether it is considered a match. A "match" means the function parameters arrived at by argument deduction are an exact match, with no implicit conversions. The process by which one function template is preferred over another is called partial ordering. Of course, there are additional rules and special cases for both partial ordering and argument deduction.

There are also areas where the writers of compilers, programs, and standards don’t always agree. It is a good idea to stay away from these areas. For example, consider these two function templates:

template<class T>
int foo(T t);

template<class T>
int foo(const T& t);

Given a call to these functions, such as the following:

MyClass mc;
foo(mc);

some compilers will report that the call foo(mc) is ambiguous. Others will compile just fine and pick the second function for execution.

Here is another example:

template<class T>
int foo(T*);

template<class T>
int foo(T&);

Given this call:

MyClass * mc = new MyClass();
foo(mc);

different compilers will report that foo(mc) is ambiguous, or compile but disagree with each other about which function actually gets called. Staying away from these fringe areas will make you a happier engineer. For a more precise description of the rules on how function templates are selected, see the C++ Standard under "Partial Ordering of Function Templates" (Chapter 14, Section 5.5.2) and "Template Argument Deduction" (Chapter 14, Section8.2).

March 2001/Using Template Functions to Customize Library Behavior

Using Template Functions to Customize Library Behavior

Michael B. Yoder

If you’re writing code to be used by other programmers, you’ll want to make it customizable, and you’ll want to do it well. This article presents an approach that is both efficient and easy to manage.


Introduction

One of the many challenges of writing a C++ library is anticipating the many different ways clients will use your code. Despite your best prophetic efforts, however, the real world always exposes the unforeseen. Unfortunately, by the time your library is in the hands of users, implementing enhancements can become difficult. Conformance to a specification, a frozen API, or backward compatibility can be serious obstacles to how you go about adding functionality.

Even if your library isn’t technically bound, it can become effectively so restrained by widespread usage in client code. One faction of users who need an enhancement might be perfectly willing to undergo the cost of source code changes and/or recompilation, but others might not be so enthusiastic.

As such, library developers are always looking for ways to design for future customization. When looking to leave algorithms or policies open, developers may turn to the Strategy Pattern [1] or the now canonical approach used in the C++ Standard library known as traits [2]. Depending on what modifications you are in a position to make, it could be more difficult to take advantage of these once your classes are in use. A cutting-edge and less invasive way of applying traits to hierarchies of classes is discussed in Andrei Alexandrescu’s recent article "Traits on Steroids." [3]

This article looks at function templates and how they can be used to provide hooks for customizing behavior.

Looking for a Solution

Consider the following working example. Assume you want to customize a policy in your classes, perhaps to specify the handling of a particular resource. Resources may include such things as file handles, network connections, mutexes, or memory. Of course, you want a solution that will give users maximum flexibility yet keep the common case trivial.

Adding new template or function parameters after the fact is not really a solution. It provides flexibility, but could break backward compatibility or require user source code changes. If users are vested in the status quo, as described above, or if they differ in basic priorities, such as space vs. time efficiency, you need to look at other solutions. I will examine three.

Using the Preprocessor

A quick and dirty method to accommodate policy change is to use macros. In Listing 1, you will notice that the macro is placed in what would be a private library header file. It is braced with an #ifdef so that the default value may be overridden. The container class is really more complex than needed here, but I introduce it this way so I can refer to it throughout this article in other examples. The container class uses the macro in the member function doPolicy, which is called whenever the policy needs to be executed.

In this contrived example, the user code requests a service from the class, which calls the doPolicy method to complete the demonstration. The output of this program is:

Policy is: default policy

The programmer can then use the compiler’s facility for predefining macros to change the value of CLASS_POLICY to something else. For example, on my compiler, if you use the flag -DCLASS_POLICY='my policy' at the command line when compiling both the library and user code, the output dutifully reports:

Policy is: my policy

As you no doubt have realized by now, the quick and dirty way is not the best way. While there might be an occasional user who doesn’t mind this method, overall it doesn’t critique well. For one thing, the policy must be set in both the library and the client code by way of recompilation. For another, the method is not practically extensible. What if you wanted to set different policies for different classes or groups of classes, or to exercise control at run time? Creating a list of macros would be awkward at best.

But this route has an even darker side-aspect. Not only would users have to recompile both the library and client code to change the policy, they would need to ensure that all their defines were 100% consistent. Failure to do this would lead to different definitions of the function in various translation units — a violation of the One Definition Rule [4]. This could easily result in unpredictable behavior. Worse yet, these errors would likely be silent, their presence going undetected by the compiler and linker.

And of course, run-time control with a preprocessor mechanism is impossible. In short, using macros is unpleasant for everyone involved.

Using a Run-time Service

Another tack is to have the library provide a service for registering new policies. All the subscribing classes would forward their policy requests to a Singleton [5, 6] provided by the library, which would in turn use either a default policy or a registered policy. Listing 2 shows an implementation of this approach.

The singleton would know each class by an identifier which would accompany its policy request. Users could override the default behavior by providing a callback function to which policy requests would be forwarded, and registering it with the policy manager. Of course, in the real world, these global callback functions would be better off in a namespace, and the static map structure used for the registry would need to be guarded for multithreaded access. The output of the user program for this example is:

Performing Default Policy.

Performing My Policy.

This method offers at least one advantage over macros. Before registering, the default is executed; afterward, the call is forwarded to the function supplied by the user. In this setup, users gain at least some degree of control. Behavior can be specified at run time with no need to recompile the library.

Things are still not as nice as we would like, however. Although these customized policies are known at compile time, the program still must pay for a run-time lookup of their policy function. In addition, the hard-coded string that each class passes in is a poor subset of the unbounded set of types that it would be possible to choose from, and is in fact limited to one customization per class template.

Using Function Templates

Function templates provide the degree of control required. Compile-time decisions can be enforced, while leaving run-time customization as an option. With templates, it is possible to set up compile-time customization to handle both distinct types and related groups of types as well. Function templates do behave a bit differently than class templates in C++, however. If you are interested in learning more about how they work, see the sidebar "Understanding Function Template Selection."

Listing 3 shows the use of function templates in action. In the library, the policy function becomes a function template instead of a member function. The MyContainer member function doPolicy is modified from the previous example to call policy with its this pointer as an argument. The use of the this pointer ensures that the policy function will always be called with the container’s specialized type as an argument.

In the user.cpp file, the user provides an overload for policy which is a more specialized function template than the primary template defined in library.h. The compiler will use this new template if policy is called with any argument that is a specialization of MyContainer on Element2<T> types. Listing 3 once again gives familiar results, but without the run-time lookup cost:

Performing Default Policy

Performing My Policy

Using function templates in this way allows you to transcend the other solutions. Now you can customize on a specific type or type grouping at user compile time. Run-time control can be reserved for fine-grained behaviors, down to instances of classes. In Listing 4, I bring back the registration service, which now allows users to implement control over class instances at run time. This function implements behavior for different instances using the same scheme as in Listing 2. In this version, however, a run-time lookup cost need be paid only if a policy is based on run-time information. Listing 4 demonstrates both compile-time and run-time policies used together.

The user program provides overloads of policy that are more specialized. The first overload is more specialized on arguments of any MyContainer<T> type, and it provides a different compile-time policy. The second overload is more specialized than the first overload; the compiler will select it when the argument is a MyContainer specialized on any Element2<T> type. This overload provides an example of run-time policies. The output of Listing 4 shows different policies being executed according to the different types and instances being used:

Performing Compile Time Policy for types of:
MyContainer<Element1<T>
    on Object: MyContainer<Element1<int> >

Performing Runtime Policy:
    Object: MyContainer<Element2<float> > Instance#1
    runTimePolicy1() executed
Performing Runtime Policy:
    Object: MyContainer<Element2<float> > Instance#2
    runTimePolicy2() executed
Performing Runtime Policy:
    Object: MyContainer<Element2<float> > Instance#3
    runTimePolicy3() executed

A Caveat

There is one limiting factor to consider when using function templates as described: how this technique interacts with explicit and implicit template instantiations within your library. Explicit template instantiations are sometimes intentionally placed in a library to help reduce the size of user executables. If your library takes advantage of some of its own template classes, they will be implicitly instantiated as well. If any of these classes use a template function as described, the compiler will, of course, generate the needed function. Since all this is now compiled in the library, a user’s function template will not be considered.

A workaround to prevent implicit instantiations from foiling this scheme is to use internal types in your library for its own classes. For example, if your library needs to use MyContainer<Element1<int> >, you can create a tight wrapper for int and use MyContainer<Element1<lib_int> > instead.

If the library needs to use explicit instantiations to help reduce the size of user executables, a workaround is more difficult. If the class method that uses the policy is made virtual, users needing to customize behavior could create subclasses and provide an override. The compiler would then consider anew its function template choices during compilation of the subclass type. Of course, users doing this would pay for customizing behavior by having the instantiations increase the size of their executables once again.

Function Templates and ANSI Compliance

The examples used in this article were compiled successfully, with only minor tweaking, using these compilers: g++ 2.95.2 on Red Hat Linux, Sunpro 5.1 on Solaris, aCC 3.13 on HP-UX, Visual Age 5.0 on AIX, MipsPro 7.3.1.1m on SGI Irix, and Compaq C++ 6.2-024 on Tru64. If your compiler doesn’t offer complete support for partial ordering of function templates, a likely symptom would be compiler ambiguity errors. A workaround for this problem is to provide alternate function templates that are completely specialized. For example, instead of this function template from Listing 3:

template<class T>
void policy(const MyContainer<Element2<T> > * c);

Use a completely specific version like this:

template<>
void policy(const MyContainer<Element2<float> > * c);

While this technique doesn’t allow for customizing groups of types, it does preserve the ability to customize at user compile time.

Acknowledgments

I would like to thank my friend and colleague Martin Sebor without whom this article would not have been possible. Also, thanks to Vijay Rajagopalan, the Foundation Team, and Technical Publications at Rogue Wave for their help and support.

References

[1] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns, Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995), p. 315.

[2] Nathan C. Myers. "Traits: a New and Useful Template Technique," C++ Report, June 1995.

[3] Andrei Alexandrexcu. "Traits on Steriods," C++ Report, June 2000.

[4] Bjarne Stroustrup. The C++ Programming Langauge, Third Edition (Addison-Wesley, 1997), p. 203.

[5] Gamma, et al. Design Patterns, p. 127.

[6] Scott Meyers. Effective C++, Second Edition (Addison-Wesley, 1998), p. 222.

Michael B. Yoder is a software engineer at Rogue Wave Software. For the past two and a half years he has worked as a part of the Foundation Group in teams responsible for Rogue Wave’s Tools.h++ Library and C++ Standard Library implementation. He has a B.S. in Computer Science from Oregon State University.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.