Продвинутые шаблоны C++

Немного о шаблонах C++. Подобные фокусы иногда находят применение в жизни.

Симуляция частичной специализации классов

Предположим что у нас некий класс который надо частично специализировать, по одному из параметров:
tepmlate <class T,int N> MyClass{
  ...
};

tepmlate <int N> MyClass<double,N>{
  ...
};

Симуляция

Однако ваш компилятор не поддерживает частичную специализацию шаблонов. Для выхода из этой проблемы придётся написать несколько конструкций. Прежде всего надо сделать отдельные варианты вашего класса, для каждого из необходимых типов.
template<class T,int N>class MyClass_
{
  ..
};

template<int N>class MyClass_float
{
  ...
};

template<class T>struct MyClass_Traits
{
   template<int N>struct _{typedef MyClass_<T,N>base;};
};

template<>struct MyClass_Traits
{
   template<int N>struct _{typedef MyClass_float<T>base;};
};

template<class T,int N>
class MyClass: public MyClass_Traits<T>::template _<N>::base{};

После чего класс MyClass можно использовать так же как обычно. Для нужных вам типов будут использоваться отдельные варианты классов.

Вычисление факториала на стадии компиляции

template<int n>struct fact{
   enum{value=n*fact<n-1>::value};
};

template<>struct fact<1>{
   enum{value=1};
};

Пример использования:

  int n=fact<7>::value; // n=720

Проверка, является ли тип указателем

template<typename T>class isPointer{
private:
	template<class U>struct x{
		enum{value=false};
	};
	template<class U>struct x<U*>{
		enum{value=true};
	};
public:
	enum {check=x<T>::value};
};

Пример использования:

   bool t;
   typedef int* ref;
   t=isPointer<int>::check;   // t=false
   t=isPointer<int*>::check;  // t=true
   t=isPointer<ref>::check;   // t=true

Сравнение двух типов

template<class T,class U>
struct SameType{
   enum {check=false};
};

template<class T>
struct SameType<T,T>{
   enum {check=true};
};

Пример использования:

   bool t;
   t=SameType<int,int>::check;   // t=true
   t=SameType<int,char>::check;  // t=false

Проверка возможности конвертирования одного типа в другой

template<class T,class U>
class Conversion{
	typedef char Small;
	class Big{char dummy[2];};
	static Small Test(const U&);
	static Big Test(...);
	static T MakeT();
public:
	enum {check=sizeof(Test(MakeT()))==sizeof(Small)};
};

Пример использования:

   bool t;
   t=Conversion<int,int>::check;     // t=true
   t=Conversion<int,char>::check;    // t=true
   t=Conversion<char,int**>::check;  // t=false

Проверка, является ли один класс наследником другово

template<class BASE,class CLS>
struct SubClass{
   enum{check=Conversion<const CLS*,const BASE*>::check};
};

Пример использования:

   bool t;
   class A{};
   class B:public A{};
   class C{};
   t=SubClass::check;  // t=false;
   t=SubClass::check;  // t=false;
   t=SubClass::check;  // t=true;

Как известно, си всегда позволяет преобразовать ссылку на класс, на ссылку на базовый класс. Например:
class A{} *p_a;
class B:public A{} *p_b;
p_a=p_b; // эта строка не вызовет ошибки

Чем собсвенно и пользуется данный шаблон. Он проверяет совместимость ссылок на классы. Есть правда незначительное ограничение. Наследование должно быть публичным, иначе преобразование ссылок не срабатывает.

Проверка, является ли тип классом

template<typename T>
class Class{
	typedef char One;
	typedef struct {char a[2];}Two;
	template<typename C>static One test(int C::*);
	template<typename C>static Two test(...);
public:
	enum {check=sizeof(test<T>(0))==sizeof(One)};
};

Пример использования:

   bool t;
   class A{};
   t=Class<int>::check;  // t=false;
   t=Class<A>::check;    // t=true;

Как известно Си позволяет получить указатель на член класса (ввиде целого числа), и обратно (член класса по указателю). One test(int ..) - пытается представить целое число как указатель на член класса и получить этот член по указателю. Если это удаётся, то мы имеем дело с настоящим классом.

Классы от которых нельзя наследоваться

Виртуальное наследование. При обычном наследовании по такой схеме:
class A{};
class B:A{};
class C:A{};
class D:B,C{};

При обычном наследовании все данные базового класса содержатся внутри производных. Т.е. выглядит это так:

Если же мы применяем виртуальное наследование, то наследуемый класс заносится в виртульаную таблицу класса и два раза нигде не дублируется:
class A{};
class B:virtual A{};
class C:virtual A{};
class D:B,C{};

Проще говоря в классе D содержится только один экземляр класса A.

При таком наследовании есть небольшой побочный эффект. При обычном наследовании конструктор базового класса вызывается из класса производного. Т.е. D вызывает конструкторы классов B и C. B и C будут вызывать конструторы своих классов A. В то время как при виртуальном наследовании, конструктор виртуального класса вызовет то класс который создаётся. Т.е. в виртуальном варианте, D сам вызывает все три конструтора A,B,C. Чем и можно воспользоваться, сделав контруктор виртуального класса приватным.

Кстати в помощью виртуального наследования можно делать следующую вещь, которая при обычном наследовании недопустима:
class A{};
class B:virtual A{};
class C:virtual A,B{};

Так вот о классах, от которых нельзя наследоваться. Прежде всего нам придётся объявить некий общий для всех класс, занести в него в качестве друзей все классы, которые мы хотим использовать для это трюка.
class Final1;
class Final2;

class Sealed{
   Sealed(){}
   friend Final1,Final2;
};

class Final1:virtual Sealed
{
   ...
};

class Final2:virtual Sealed
{
   ...
};

Так вот, поскольку в классах Final1, Final2 используется виртуальное наследование. То классы которые захотят унаследоваться от Final1, Final2, должны будут сами вызывать конструтор класса Sealed, что делать нельзя, потому как конструктор в привате. Следовательно компилятор не позволит использовать классы унаследованные от Final1, Final2.

следующий шаг

Можно избавится от утомительного перечисления дружественных классов с помощью шаблона:
template<class T>class Sealed
{
	friend T;
	Sealed(){}
};

class Final1:virtual Sealed<Final1>
{
   ...
};


Что кстати будет давать более понятные сообщения об ошибках, при попытке наследоваться от Final1, Final2.

следующий шаг

Можно избавится от упоминания виртульного наследования, при каждом использовании класса Sealed.
template<class T>class Sealed;

template<class T>class Sealed_
{
	friend T;
	friend Sealed<T>;
	Sealed_(){}
};

template<class T>class Sealed:virtual Sealed_<T>{};


class Final1:Sealed<Final1>
{
   ...
};

Правда есть одна неприятность. В стандарт C++ не сказано что шаблон может объявлять свой параметр, как дружественный класс. Следовательно не все компиляторы позволят использовать трюки подобные шаблоны. насколько я знаю, любые версии Visual C++ (старые и новые) это позволяют. gcc 4 - не позволяет. gcc 3 - может быть позволяет, проверить это пока не могу. В крайнем случае можно пользоваться вариантом без шаблонов.

Выяснение размера массива

template<int N>struct _array_inside{char data[N];};

template<class T,int N>_array_inside<N> const &_array_size(T (&)[N]);

#define sizeof_array(array) sizeof(_array_size(array).data);
Можно упростить задачу (рискнув тем что оптимизатор может не скушать процедуры и увеличить код):
template<class T,int N>inline const int sizeof_array(T (&)[N])
{
	return N;
}

Пример использования:

int count;
struct my{int a; double b;};

my array1[37];
count=sizeof_array( array1); // count=37

int array2[5][3][7];
count=sizeof_array( array2); // count=5
count=sizeof_array(*array2); // count=3

Вся прелесть метода заключается в том, что там не надо знать тип элементов массива. Но у этого макроса есть два недостатка. 1) он не работает в старых версиях Visual C++. 2) он не работает если массив создан из структуры, которая объявлена локально внутри функции. Структуру придётся определить глобально или внутри класса.

Вариация на тему

Можно сделать небольшую модификацию метода. Класс который превращает многомерный массив в одномерный. Бестолково, но может быть интересно. Правда с ним справится далеко не всякий оптимизатор, поэтому будет генериться дополнительный код:
struct Array{
	template<class T,int N>static int size(T (&)[N])
	{
		return N;
	}
	template<class T,int N,int M>static int size(T (&x)[N][M])
	{
		return N*size(x[0]);
	}
	template<class T,int N>static int get(T (&x)[N],int index)
	{
		assert(index<N);
		return x[index];
	}
	template<class T,int N,int M>static int get(T (&x)[N][M],int index)
	{
		int s=size(x[0]);
		return get(x[index/s],index%s);
	}
};


int arr[3][5][7],n=0;
for(int x=0; x<3; x++)
for(int y=0; y<5; y++)
for(int z=0; z<7; z++) arr[x][y][z]=n++;

for(n=0; n<Array::size(arr); n++) printf("%d(%d) ",Array::get(arr,n),n);

О жизни

Вообще лично у меня очень простое отношение к жизни. Если вам нужен код который зависит от типа данных, это хорошо. Можно применить шаблон, но писать его надо с умом, не забывая того что в итоге должен быть живой код (а не мутант с кучей клонированного кода). Если вы не хотите наследоваться от класса, просто не наследуйтесь. Если сам нужен синглтон, то почему-то просто не заменить класс на пространство имён? И не городить объект с кучей ненужной функциональности, просто ради того чтобы иметь какие-то глобальные функции.

Один пример живого применения шаблонов. Программа для расчёта контрольной суммы, любой разрядности (без потери качества).

crc_all2.cpp

Hosted by uCoz