Introduction
Combining templates and inheritance opens new ways for creating power techniques in C++ [1-3]. This article presents a new technique to avoid code duplication in some special cases and provides examples to illustrate it. The technique is very simple, and it is surprising that it has not yet been published.
Motivation
The main motivation behind this technique is to provide a new way to avoid code duplication.
Definition
We will follow standard template terminology. A reference for this terminology can be found in [4,§45].
Note the distinction between the terms class template and template class:
Class template is a template used to generate template classes. You cannot declare an object of a class template.
Template class is an instance of a class template.
Starting point
Consider the following simple construction:
template <class T>
class Derived : public Base (1)
The class template Derived
can consist of any member functions, members, static data members, and so forth, but the members of the class do not depend on the template parameter T
in any way. So, in a sense, T
is a fake parameter and simply can be omitted:
template <class>
class Derived : public Base (1-a)
For any specialization of the class Derived
, we need to provide a specific parameter T
. The parameter itself does not play any role; it is used only for specialization and in that case it will simply be an empty structure:
struct Derived1_Tag (2)
{};
The name of the type reflects that the fake parameter resembles a Tag. The next (optional) step is to provide some naming convention to connect the Tag and the template specialization. We will associate the tag name with the name of the template class. One of the easiest ways to do this by using typedefs:
typedef Derived<Derived1_Tag> <derived1_tag /> Derived1; (3)
Thus, the template argument includes the name of the derived template class by convention. We will name the idiom (1-3) as TFP idiom (Template with a fake parameter) for the rest of the article.
That is all that the technique consists of. It is very simple, but next we will see when and how it can be used.
Using the Technique
This technique can be used in the following cases:
- A set of derived classes that is all identical except for class name.
- A set of derived classes that differ slightly from each other in terms of behavior.
Let's look at the second case. Suppose we have a base class:
class Base
{
virtual void foo1() =0;
virtual void foo2() =0;
};
And 3 derived classes:
class Derived1 : public Base
{
virtual void foo1();
virtual void foo2();
};
class Derived2 : public Base
{
virtual void foo1(); virtual void foo2();
};
class Derived3 : public Base
{
virtual void foo1();
virtual void foo2(); };
The other details are omitted, but suppose the derived classes are the same except that they have different implementations of the functions foo1() and foo2().
In that case, we follow these steps:
Step 1: Replace Derived classes with a class Template with fake parameters. As a default implementation for function foo1() and foo2() take the implementation of the class Derived1:
template<class><class />
class Derived : public Base
{
virtual void foo1(); virtual void foo2(); };
Step 2: Define empty structs for all Derived classes.
struct Derived1_Tag
{};
struct Derived2_Tag
{};
struct Derived3_Tag
{};
Step 3: (optional, but useful) Using typedef, provide user-friendly names for the classes:
typedef Derived< Derived1_Tag> Derived1;
typedef Derived< Derived2_Tag> Derived2;
typedef Derived< Derived3_Tag> Derived3;
Step 4: Specialize Derived functions that are different for the classes.
template <>
void Derived2::foo1()
{
}
template <>
void Derived3::foo2()
{
}
The interesting detail is that the specialization of member functions plays the same role as overriding.
Let's me stress that it is legal to specialize single member function without specializing all member functions.
According to the C++ Standard [5,§14.7.3] :
"A member function, a member class or a static data member of a class template may be explicitly specialized for a class specialization that is implicitly instantiated."
If you have problem with template terminology such as "explicit instantiation","implicit instantiation", "partial specialization" and so forth, please consult [4,§45-48]. If you do not want to go deeper, just remember that the standard allows you to specialize some of the member functions without specializing all of them.
Let's illustrate the technique with some more examples.
Replacing Macros
As a first example, we will consider a class Exception. This idea is taken from Xerces library [6] and the class is simplified it for the purposes of illustration.
Let us observe how exceptions are implemented in the library. They provide one Base class like this (the real implementation of Xerces Exception is different):
class BaseException
{
public:
BaseException();
BaseException(const std::string &what);
virtual ~BaseException() = 0;
BaseException(const BaseException& e);
BaseException& operator=(const BaseException& e);
virtual const char* what() const;
private:
std::string what_;
};
All other exception classes are identical except for the name of the exception. Since it is tedious to type them repeatedly, the Xerces authors replace them with Macros:
#define MakeDerivedException(DerivedException) \
class DerivedException : public BaseException \
{ \
public: \
\
DerivedException() : BaseException(){ } \
\
explicit DerivedException( const char* what) : \
BaseException(what){ } \
\
explicit DerivedException(const std::string &what) : \
BaseException(what) { } \
\
virtual ~DerivedException() {}; \
\
DerivedException(const DerivedException& e) : \
BaseException(e) { } \
\
DerivedException& operator=(const DerivedException& e) \
{ \
BaseException::operator=(e); \
return *this; \
} \
};
Using Macros it is very easy to define any client exception,for example:
MakeDerivedException (XSerializationException)
Let's apply TFP idiom.
The base class doesn't change. The Macros will be replaced with class template:
template <class>
class DerivedException : public BaseException
{
public:
DerivedException() : BaseException(){ }
explicit DerivedException( const char* what) :
BaseException(what){ }
explicit DerivedException(const std::string &what) :
BaseException(what) { }
virtual ~DerivedException() {};
DerivedException(const DerivedException& e) :
BaseException(e) { }
DerivedException& operator=(const DerivedException& e)
{
BaseException::operator=(e);
return *this;
}
};
We omit template parameter since it doesn't contribute to definition of DerivedException
.
Now you can define a client Exception by a specialization of DerivedException
:
struct XSerializationException_Tag{}
typedef DerivedException< XSerializationException_Tag> XSerializationException;
Sample code is provided for both cases (look on demo-projects MacroException and FakeTempleteException).
Let's try to figure out which is a better solution. From the point of usage they are equivalent. Both of them allow us to avoid redundancy.TFP idiom allows your clients to inherit from the XSerializationException class to fit their needs. You can also derive from Macros definition.
We can look at the "Pro" and "Contra" sides of TFP idiom solution, versus the Macros solution.
Contra:
- With Macro you use only one statement to define the derived class, while with TFP idiom you reach the same with two statements.
Pro:
- "The first rule about macros is: Don't use them unless you have to." [7,§7.8]. You can consult C++ books [3,§2], [8,§16] for why it is generally better to avoid macros.
- From practical point of view, TFP idiom allows you to debug your code with the compiler, while the macro solution does not.
Applying with Clone Classes
Clone classes arise in class hierarchies with a member function that creates an exact copy of derived object. It sometimes called "virtual constructor" in C++ [4], or, more generally, a Prototype pattern [9].
Consider the following prototype diagram:
The base abstract class IDataReader
represents a prototype that declares an interface for cloning itself. Child classes implement an operation for cloning themselves, and the client creates DerivedReader
s by asking prototype to clone itself.
Let's assume our Readers obtain some data from different sources. The sources can be files with different formats or data from a database. Thus, a specific reader has read a function that fills out some container, for example, a vector of strings.
Depending on particular data, Reader can return true
or fa
lse
, for example, if we check the uniqueness of some record in a table. So, we select data, and if we have more then one record with same ID, the reader should return false
. Or, we need to retrieve a particular record from a table by some id. In the case that this record actually exists, we return true
, and if not, we return false
.
Generally speaking, our readers are very similar except for the implementation of the read function. Let's go back and provide some implementation for the classes.
Following the example in [8,§54], consider the sample code for the base class:
class IDataReader
{
public:
typedef std::auto_ptr<idatareader /> IDataReaderPtr;
typedef std::vector<:string> VecString;
IDataReader();
virtual ~IDataReader() = 0;
IDataReaderPtr clone() const
{
doClone();
IDataReaderPtr p = doClone();
assert(typeid(*p)==typeid(*this)&&"doClone incorrectly overriden");
return p;
}
bool read(VecString& outs)
{
return doRead(outs);
}
protected:
IDataReader(const IDataReader& reader);
virtual IDataReaderPtr doClone() const = 0;
virtual bool doRead( VecString& outs) = 0;
private:
IDataReader& operator=(const IDataReader&);
};
Let's look on some details of the implementation.
First, non-virtual interface (NVI) idiom [3,8] is used for the design. The base class defines the interface by non-virtual functions clone() and read() that call protected virtual function counterparts doClone() and doRead(). One of the advantages of using NVI is it allows us to include type checking in the clone() function. It will remind us if a further derived class doesn't implement the doClone() function or if the function doesn't return the object of the IDataReader type.
Next, instead of returning a pointer from the clone() function, we return auto_ptr (follow "source" idiom [10,§37]). The advantage is that it is a completely safe way to let the caller know about the ownership of the pointer. Even if the caller ignores the return value, the allocated object will always be safely deleted.
Now, let's start to implement our Derived classes. We need to provide for any Derived class constructor, destructor, copy of constructor, doClone() and doRead() functions, and explicitly disallow the assignment operator. For example, for our first Derived class:
class DerivedReader1 : public IDataReader
{
typedef IDataReader::IDataReaderPtr IDataReaderPtr;
public:
DerivedReader1() : IDataReader() {}
virtual ~DerivedReader1() {}
protected:
DerivedReader1(const DerivedReader1& rhs) :
IDataReader(rhs) { }
virtual IDataReaderPtr doClone() const
{
IDataReaderPtr ptr(new DerivedReader(*this));
return ptr;
}
virtual bool doRead( VecString& outs);
private:
DerivedReader& operator=(const DerivedReader&);
};
As soon as we start to write our next derived classes we will see that we repeat the same code again and again. The code of one derived class differs from another only by the doRead() function. The other code is redundant.
So, following TFP idiom, we override the code:
template <class>
class DerivedReader : public IDataReader
{
};
create the tags :
struct DerivedReader1_Tag{};
struct DerivedReader2_Tag{};
and specialize classes:
typedef DerivedReader<derivedreader1_tag /> DerivedReader1;
typedef DerivedReader<derivedreader2_tag /> DerivedReader2;
Thus, instead of duplicating, we can work on implementation of doRead() functions for derived classes. For example,
template <>
bool Derived1Reader::doRead( VecString& outs)
{
std::cout << "Call Derived1Reader::doRead" << std::endl;
outs.push_back("dummy_data1");
return !outs.empty();
}
template <>
bool Derived2Reader::doRead( VecString& outs)
{
std::cout << "Call Derived2Reader::doRead" << std::endl;
outs.push_back("dummy_data2");
return outs.empty();
}
In our code sample, we create upfront reader objects by "Cloning Factory" that map object with its name. The client calls the Factory create function and obtains a derived class by name. For more advanced Factory examples, see [1,§8].
Finally, here the prototype diagram in the TFP case:
Final Remarks
The TFP idiom can be useful in combination with several patterns: Prototype, Command,Template Method and etc[9] when your derived classes differ by behavior.
The main limitation in technique is that you can not introduce additional members of derived classes without full template specialization.
The "fake" or empty template parameters were used for specifying policies with template template parameters by Alexandrescu[1]. The idea of the empty tag comes from STL library, where the tag convention is used for design iterators [7].
As far as I know, the TFP idiom technique is as of yet unpublished.
I hope that this technique can be useful in your projects.
Acknowledgments
I would like to thank Philip Eskelin for discussion of the matter of the article . Thanks to my son Tim Kunisky for helping to prepare the article.
History
23 August 2007
Remove the statement(Thanks to my coworker Alex Urben who find the mistake.)
:
With the Macro definition, any Derived class is final. For example, we would not be able to derive from the macro implementation of the XSerializationException class.
References
- [1] Andrei Alexandrescu, Modern C++ Design: Generic Programming and Design Patterns Applied. Addison-Wesley, 2001
- [2] David Vandevoorde, Nicolai M. Josuttis, C++ Templates: The Complete Guide. Addison-Wesley, 2002
- [3] Scott Meyers, Effective C++ (3rd edition). Addison-Wesley, 2005
- [4] Stephen C. Dewhurst, C++ Common knowledge. Addison-Wesley, 2005
- [5] International Standard for C++, ISO/IEC, 1998
- [6] Xerces-C++ parser, http://xml.apache.org/xerces-c/
- [7] Bjarne Stroudstrup, The C++ Programming Language (3r d edition), Addison-Wesley, 1998
- [8] Herb Sutter, Andrei Alexandrescu, C++ Coding Standards, Addison-Wesley, 2005
- [9] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns, Addison-Wesley, 1995.
- [10] Herb Sutter, Exceptional C++, Addison-Wesley, 2002