C++ Tips : rev. 2006-05-19 by FipS
This place is devoted to collect my everyday C++ experience.
Tip #6 - Static polymorphism, Interfaces & CRTP, 2006-05-19
Modular systems usually involve some kind of interface abstraction.
The most common approach involved is interfaces realized through ABC
(Abstract Base Classes). On a certain level of
abstraction it is the ideal solution, it offers dynamic binding, crossing
DLL boundaries etc. The negative site of the approach is a small
real-time overhead introduced by virtual function calls.
Most of the time it is insignificant but deeper inside a module e.g.
in the inner loops or inside an algorithm, the overhead could become
significant.
In those situations especially where the types involved are known in the
compile-time one should consider using of Static polymorphism
through C++ templates instead of ABC. The example below
shows a compile-time alternative to ABC. Moreover, CRTP
(Curiously Recurring Template Pattern) idiom
is introduced to retain common implementation in the base class (interface).
Such a programming construct doesn't require virtual function calls so
there is a good chance to inline some of the functions to boost
performance.
#include <iostream>
template <typename T>
struct Interface
{
void Polymorphic() const // compile-time polymorphic call
{ static_cast<const T *>(this)->PolymorphicImpl(); }
void Common() const // common implementation
{ std::cout << "Common()\n"; }
};
struct Concrete1 : public Interface<Concrete1>
{
void PolymorphicImpl() const // concrete implementation #1
{ std::cout << "Concrete1::Polymorphic()\n"; }
};
struct Concrete2 : public Interface<Concrete2>
{
void PolymorphicImpl() const // concrete implementation #2
{ std::cout << "Concrete2::Polymorphic()\n"; }
};
template <typename TIfc>
void Polymorphic(const TIfc &Ifc)
{
Ifc.Polymorphic(); // call through the interface
}
int main()
{
Concrete1 c1;
Concrete2 c2;
Polymorphic(c1); // polymorphic call of concrete implementation #1
c1.Common(); // call of common implementation
Polymorphic(c2); // polymorphic call of concrete implementation #2
c2.Common(); // call of common implementation
}
// Concrete1::Polymorphic()
// Common()
// Concrete2::Polymorphic()
// Common()
|
[ Comments here... ]
Tip #5 - TSInt2Type<> as a Compile-Time Code Switch, 2006-03-15
Today let me introduce you a simple but very useful C++ template -
TSInt2Type. Originally it comes from Andrei Alexandrescu's -
[ Modern C++ Design: Generic Programming and Design Patterns Applied ] book.
template <int n>
struct TSInt2Type
{
enum { Val = n };
};
|
The template generates a distinct type for each distinct constant
integral value passed /AA/. You can use this feature in
situations whenever you want to perform a compile-time code switch based
on a compile-time constant. The example below shows this technique. It's a
fragment of a static type system. There is an interface class 'IObject'
and an implementation class 'SObject'. The latter has a static
function 'Create', which is indicated by a compile-time constant
'IsImpl = 1'. The type system represented by 'TSTypeInfo' then needs to
decide whatever or not a certain type 'T' has the create function.
At this point TSInt2Type comes to help.
The template provides a constant-based type through 'TSInt2Type<T::IsImpl>()' which
resolves the active code branch defined by a couple of overloaded template
functions. The important feature of the compiler is that a template
function, which is never used, isn't compiled at all! So the only active
code branch has to be compilable!
#include <iostream>
// THE MAGIC TEMPLATE, generates a type for a compile time constant passed
template <int n> struct TSInt2Type { enum { Val = n }; };
// requested function pointer
typedef struct IObject * (*TCreateFuncPtr)();
// overloaded template functions by a constant-based type
template <typename T>
TCreateFuncPtr ResolveCreateFunc(TSInt2Type<0>) { return 0; }
template <typename T>
TCreateFuncPtr ResolveCreateFunc(TSInt2Type<1>) { return &T::Create; }
// static type info
template <typename T>
struct TSTypeInfo
{
TSTypeInfo()
{
// run-time switch below, isn't compilable for T = 'IObject' !!!
//TCreateFuncPtr pFunc = T::IsImpl ? return &T::Create : 0;
// >>> >>> >>>
// COMPILE-TIME SWITCH compiles fine!
TCreateFuncPtr pFunc = ResolveCreateFunc<T>(TSInt2Type<T::IsImpl>());
// <<< <<< <<<
std::cout << "0x" << pFunc << std::endl;
}
};
struct IObject // abstract interface
{
enum { IsImpl = 0 }; // compile-time constant
static TSTypeInfo<IObject> m_Info; // 'IObject' type info
virtual void Foo() const = 0;
};
TSTypeInfo<IObject> IObject::m_Info; // cpp
struct SObject : public IObject // implementation
{
enum { IsImpl = 1 }; // compile-time constant
static TSTypeInfo<SObject> m_Info; // 'SObject' type info
static IObject * Create() { return new SObject; }
virtual void Foo() const {}
};
TSTypeInfo<SObject> SObject::m_Info; // cpp
int main() {}
// 0x00000000 // the result of 'return 0' version
// 0x0041919A // the result of 'return &T::Create' version
|
* 2006-03-20 according to [ Q240871 ] there is a bug in MSVC 6 compiler: If all the
template parameters are not used in function arguments or return type of a template
function, the template functions are not overloaded correctly! Solution:
Use dummy arguments to the function. You can find a workaround below:
// VC6 bug fixed version
template <typename T>
TCreateFuncPtr ResolveCreateFunc(TSInt2Type<0>, const T* = 0)
{ return 0; }
template <typename T>
TCreateFuncPtr ResolveCreateFunc(TSInt2Type<1>, const T* = 0)
{ return &T::Create; }
|
[ Comments here... ]
Tip #4 - Algorithms, Traits & Policies, 2005-11-13, rev. 2006-02-23
The example below shows a simple version of an algorithm using the technique of
Traits and Policies. It is a STL-like generic
way to allow user to strongly customize types and algorithms.
Traits brings an association between a concrete type and an extra type
information. In the example below, a type 'unsigned char' is bound
with the storage type 'int' (TStorage)', in other words, whenever the
algorithm wants to know the apropriate storage type for 'unsigned char'
it uses 'TSTraits' to find out.
Policies provide an interface, which is involved in the algorithm. In
the example below, a policy is used to specify the operation ('summation'
or 'counting'), which is applied to all items of the sequence.
#include <iostream>
template <typename TElem> // TRAITS: defines traits of an element type
struct TSTraits;
template <> // TRAIT: binds 'uchar' element type with 'int' storage type
struct TSTraits<unsigned char>
{
typedef int TStorage;
static TStorage Zero() { return 0; }
};
struct TSSumPolicy // POLICY: defines a 'sum' variant of the algorithm
{
template <typename TStorage, typename TElem>
static void Operation(TStorage &Storage, const TElem &Elem)
{ Storage += Elem; }
};
struct TSCountPolicy // POLICY: defines a 'count' variant of the algorithm
{
template <typename TStorage, typename TElem>
static void Operation(TStorage &Storage, const TElem &) { ++Storage; }
};
template<typename TElem, typename TPolicy, typename TTraits =
TSTraits <TElem> >
struct TSAlgorithm // ALGORITHM: customized by traits & policies
{
typedef typename TTraits::TStorage TStorage;
static TStorage Execute(TElem *pBegin, TElem *pEnd)
{
TStorage Storage = TTraits::Zero();
while(pBegin != pEnd)
{ TPolicy::Operation(Storage, *pBegin); ++pBegin; }
return Storage;
}
};
int main()
{
unsigned char auArray[] = { 100, 110, 120, 130, 140 };
// calc a sum of the array items using 'TSSumPolicy'
int nSum = TSAlgorithm<unsigned char, TSSumPolicy>::Execute
(&auArray[0], &auArray[5]);
std::cout << "sum = " << nSum << std::endl;
// calc a number of items of the array using 'TSCountPolicy'
int nCount = TSAlgorithm<unsigned char, TSCountPolicy>::Execute
(&auArray[0], &auArray[5]);
std::cout << "count = " << nCount << std::endl;
return 0;
}
// sum = 600
// count = 5
|
[ Comments here... ]
Tip #3 - 'std::for_each' and functor adapter 'std::mem_fun', transl. 2005-06-18
Nowadays it is hard to imagine an application written without using of the STL collections.
The collections are typically used as a storage of various object instances or instance pointers.
Sometimes it is need to invoke an operation over all stored instances. It is typically
done by calling a member function in a loop.
However, STL offers more elegant and potentially efficient way to do that. The algorithm
'std::for_each' and functor adapter 'std::mem_fun' or 'std::mem_fun_ref' are used.
The algorithm 'std::for_each' calls a user defined function in a range of iterators, functor
adapter 'std::mem_fun...' transforms function call to the call of a member function of an object.
The difference between 'std::mem_fun' and 'std::mem_fun_ref' is that the first one is used when
a collection of object instance pointers is expected while the second one is used for collection
of embeded instances. 'std::mem_fun...' can call a member function with no argument. With the help of
another functor adapter 'std::bind2nd' calling of a member function with one argument can
be reached. Note that current STL standard doesn't support more then one argument. The example
below shows the technique.
Note. [ Boost ] library offers alternatives
to 'std::mem_fun...' that can call member functoins without limitation of the number of
arguments. For more information see Boost's [ Bind ] and [ Lambda ].
#include <stdio.h>
#include <vector>
#include <algorithm> // std::for_each
#include <functional> // std::mem_fun, std::mem_fun_ref, std::bind2nd
struct STest
{
void Print() { printf("0x%08x\n", (size_t)this); }
void PrintValue(int nValue) { printf("nValue = %d\n", nValue); }
};
int main()
{
// vector of object instances
std::vector<STest> Tests;
Tests.push_back(STest());
Tests.push_back(STest());
Tests.push_back(STest());
// vector of object instance pointers
std::vector<STest *> TestPtrs;
TestPtrs.push_back(&Tests[0]);
TestPtrs.push_back(&Tests[1]);
TestPtrs.push_back(&Tests[2]);
printf("std::mem_fun_ref:\n");
// no argument call
std::for_each(Tests.begin(), Tests.end(),
std::mem_fun_ref(&STest::Print));
// 1 argument call
std::for_each(Tests.begin(), Tests.end(),
std::bind2nd(std::mem_fun_ref(&STest::PrintValue), 10));
printf("\nstd::mem_fun:\n");
// no argument call
std::for_each(TestPtrs.begin(), TestPtrs.end(),
std::mem_fun(&STest::Print));
// 1 argument call
std::for_each(TestPtrs.begin(), TestPtrs.end(),
std::bind2nd(std::mem_fun(&STest::PrintValue), 10));
return 0;
}
// std::mem_fun_ref:
// 0x00322c70
// 0x00322c71
// 0x00322c72
// nValue = 10
// nValue = 10
// nValue = 10
//
// std::mem_fun:
// 0x00322c70
// 0x00322c71
// 0x00322c72
// nValue = 10
// nValue = 10
// nValue = 10
|
[ Comments here... ]
Tip #2 - Member references and 'operator =', transl. 2005-04-16
In an effort to produce a robust code, there may occur member items in a form of references
in our objects (it is seen in the example below 'm_rnValue'). Initialization of the items
is possible only in the constructor. If we need to implement the assignment operator
'operator =' (for example STL collections require the operator to allow to store the
objects in), there is a problem. Assignment in a way 'm_rnValue = RHS.m_rnValue' is not
possible, it only copies a value of the item not the reference! (for the const references
it is not possible at all). There is an interesting solution that allows to implement the
assignment operator correctly in such situations. Firstly, the destructor is explicitly
invoked then a new instance of the object is created at the same memory address (the
placement new is used). The method is seen in the example below. Note that there are some
limitations when it is used in inherited objects (it is important to implement the
assignment operator properly in all the inherited objects).
class CTest
{
public:
CTest(const int &nValue): m_rnValue(nValue) {}
CTest(const CTest &RHS) : m_rnValue(RHS.m_rnValue) {}
CTest & operator = (const CTest &RHS)
{
if(this != &RHS)
{
// wrong !!! (for 'const' not possible at all)
// m_rnValue = RHS.m_rnValue;
this->CTest::~CTest();
new (this) CTest(RHS);
}
return *this;
}
private:
CTest();
const int &m_rnValue;
};
int main()
{
int nValue1 = 1;
int nValue2 = 2;
CTest Test1(nValue1); // Test1.m_rnValue = 1
CTest Test2(nValue2); // Test2.m_rnValue = 2
Test2 = Test1; // call 'operator ='; Test(1/2).m_rnValue = 1
nValue1 = 3; // Test(1/2).m_rnValue = 3
return 0;
}
|
[ Comments here... ]