Инстанциирование шаблонов функций по списку типов (Часть 1)

в 21:17, , рубрики: c++, ненормальное программирование, С++

Случалось ли Вам писать шаблон функции, который должен быть инстанциирован для определённого набора типов и больше ни для чего? Если нет, то эта статья врядли покажется Вам интересной. Но если Вы всё ещё здесь, то тогда начнём.

Cтатья будет состоять из двух частей. В первой части будет описана проблема и представлено её первичное, немного кривоватое, решение. Вторая часть будет посвящена усовершенствованию и обобщению изложенного решения.

Первым делом опишем проблему. Представьте себе, Вы обьявляете шаблон функции в заголовочном файле. Если шаблон должен быть потенциально пригоден для всего, что только можно, то и определить его нужно здесь же, в заголовочном файле. Это влечёт за собой сквозные зависимости, увеличение времени компиляции и срач в заголовочном файле. Но это всё же неизбежность. Конечно, можно определить шаблон в другом заголовочном файле и включить его внизу файла с обьявлением. Это избавит Вас от третьей проблемы, но не избавит от первых двух. Теперь обратная ситуация, когда шаблон должен быть использован(инстанциирован) только для парочки конкретных типов. Тогда вы смело переносите определение в исходник и явно инстанциируете Ваш шаблон для каждого отдельного типа. Немного трудоёмко в сопровождении, но всё же лучше чем гадить в заголовочном.

Наша ситуация находится где-то посередине. Есть шаблон функции, и он должен быть инстанциирован для конкретного списка типов, который где-то у Вас в проекте увековечен с помощью typedef'а. Ну, например:

typedef TypeList<int,char,bool,string, EmptyList> MyTypeList. 

О том, что такое список типов можно почитать у А.Александреску в «Современное проектирование на С++», а пример реализации — здесь.
Под катом самопальная имплементация(такая же как и у тысяч других, наверное). Она, мне лично, больше нравится, так как позволяет писать
typedef TypeList<int,char,bool,string, EmptyList> MyTypeList;
вместо классической записи
typedef TypeList<int,TypeList<char,TypeList<bool,TypeList<string, EmptyList>>>> MyTypeList;

struct EmptyList{};
template<typename Head, typename... Tail>
struct TypeList
{
    typedef Head head_t;
    typedef TypeList<Tail...> tail_t;
};
template<typename Head>
struct TypeList<Head, EmptyList>
{
    typedef Head head_t;
    typedef EmptyList tail_t;
};

Вернёмся к теме. У Вас в проекте могут находиться пятьдесят разных шаблонов функций, и каждый из них должен быть инстанциирован только для этого вездесущего списка типов. Как же поступить лучше:
1) Определить шаблон в заголовочном файле — это пораженческий настрой.
2) Определить шаблон в исходнике и специализировать его вручную для всех типов из списка… ага, а потом исправлять в 50-ти местах, если список увеличится, уменьшится или просто изменится.

Оба варианта плохи. Цель этой статьи показать, как можно определить шаблон в исходнике и избавиться от ручного инстанциирования для каждого типа. Чтобы цель стала немного понятней и пробудить у Вас аппетит, просто приведу конечный результат. Но о том, как он реализован, будет рассказано только в следующей части статьи.

-------------------------------- typelists.h  этот файл Вы уже видели выше -------------------------------- 
#pragma once
struct EmptyList{};
template<typename Head, typename... Tail>
struct TypeList
{
    typedef Head head_t;
    typedef TypeList<Tail...> tail_t;
};
template<typename Head>
struct TypeList<Head, EmptyList>
{
    typedef Head head_t;
    typedef EmptyList tail_t;
};
typedef TypeList<int, double, bool, char, const char*, EmptyList> MyTypeList;
..... а ещё тут будет всякая магия, но не сегодня ...
--------------------------------  myclass.h -------------------------------- 
#pragma once
class MyClass
{
public:
    template<typename T>
    void f(T x);
};
--------------------------------  myclass.cpp -------------------------------- 
#include "templateclass.h"
#include "typelist.h"
#include <iostream>
namespace
{
    InstantiateMemFunc(MyClass, f, MyTypeList) // (*)
}
template<typename T>
void MyClass::f(T x)
{
    std::cout<< x << "n";
}
--------------------------------  main.cpp -------------------------------- 
#include <typelist.h>
#include "myclass.h"
int main()
{
    MyClass tc;
    tc.f(3);
    tc.f(2.0);
    tc.f(true);
    tc.f('a');
    tc.f("hello world");
    return 0;
}

Надеюсь, теперь стало понятней, что есть проблема и что есть цель. Заметим, что обычно нам бы пришлось вставить следующий код в исходник myclass.cpp, чтобы данная программа смогла скомпоноваться:

template<> void MyClass::f<int>(int);
template<> void MyClass::f<double>(double);
template<> void MyClass::f<bool>(bool);
template<> void MyClass::f<char>(char);
template<> void MyClass::f<const char*>(const char*);

Здесь уже пусть каждый сам судит, что ему больше нравится, это или строчка со звёздной в myclass.cpp.

Остаток первой части статьи будет посвящен решению инстанциирования шаблона для списка типов в исходном файле. Единственное, чем будет хорошо первое решение — это тем, что оно будет работать. А вторая часть статьи откроет завесу, что же стоит за выражением InstantiateMemberFunction в файле myclass.cpp.

Итак, перейдём к делу, нам нужно инстанциировать шаблон функции для списка типов. Разобьём задачу на две подзадачи:
1) как сделать что-то для списка типов и
2) как инстанциировать шаблон для одного конкретного типа.

Начнём с первой подзадачи. Здесь, кроме как стырить приём с рекурсией, ничего в голову не приходит. Ну, например, вот так:

template<typename Types> 
struct SomethingDoer; // Класс, который что-то делает для списка типов 

template<> struct SomethingDoer<EmptyList> // Специализация для пустого списка 
{ 
    static void doSomething(...) // делает что-то для никого, т.е не делает ничего 
    {} 
}; 

template<typename Head, typename... Tail> 
struct SomethingDoer<TypeList<Head, Tail...> > // Специализация для непустого списка. 
{   
    static void doSomething() 
    { 
      ... сделать что-то для головы списка типов - Head .... (**) 
      SomethingDoer<typename TypeList<Head, Tail...>::tail_t>::doSomething(); // рекурсивный вызов для хвоста списка типов 
    } 
}; 

Теперь цель задача становится предельно ясна. В строке с двумя звёздочками нужно инстанциировать желаемую функцию для одного конкретного типа — Head.
Перейдём ко второй подзадаче.
Вопрос: Как можно инстанциировать шаблон функции? 5 секунд.
Время вышло. Ответ: явно и неявно.

Явно, как мы уже обсудили, — слишком трудоёмко в сопровождении. А что насчёт неявно?
Вопрос: назовите два способа неявного инстанциирования шаблона функции? 10 секунд. Ладно, ещё 10 десять секунд. Но теперь время вышло.
Ответ:
Способ первый — вызвать функцию так, чтобы параметр шаблона мог быть выведен либо указать его напрямую;
Способ второй — определить переменную имеющую тип инстанции шаблона и присвоить ей… ээээ, как бы это лучше сказать… адрес шаблона (да, я знаю, у шаблона нету адреса, но как это назвать по-другому я не знаю).
Пример лучше случайного набора слов:

template <typename T> void f() {...} 
template <typename T> void g(T t) {...} 
struct S 
{ 
        template <typename T> 
        void memFunc(){...} 
}; 
f<int>(); // Способ первый 
g(58); // Способ первый 
void (*gPtr)(int) = g; // Способ второй 
void (S::*memFuncPtr)() = &S::memFunc;  // Способ второй 

О том какой способ нам лучше подойдёт сильно разсусоливать не буду, а то затянется надолго. Сразу скажу — второй подойдёт лучше.
Раз Вы со мной согласны, то давайте попытаемся инстанциировать шаблон вторым способом.

template<typename Head, typename... Tail> 
struct SomethingDoer<TypeList<Head, Tail...> >  
{   
    typedef void (MyClass::*MemFuncType)(Head);
    static void doSomething() 
    {       
        MemFuncType x = &MyClass::f;
	    (void)(x); // чтобы компилятор не ныл, будто переменная не используется
    	SomethingDoer<typename TypeList<Head, Tail...>::tail_t>::doSomething();     
    } 
}; 

Вуаля, механизм инстанциирования шаблонов по списку типов создан… но ещё не приведён в действие. Представть себе, Вы написали вышеприведенное определение класса SomethingDoer вместе с его методом doSomething где-нибудь в безымянном пространстве имён файла-исходника, в данном случае в myclass.cpp. Будет ли тем самым шаблон MyClass::f(T) инстанциирован для желаемого списка типов? К сожалению, нет. Но как же заставить вышепреведенный код делать то, для чего он был создан. Да, очень просто. Нужно его вызвать:

SomethingDoer<MyTypeList>::instantiate(); 

Только вот где эту чудесную строчку написать? Там же в безымянном пространстве имён? Она ведь ничего не возвращает, её нельзя присвоить никакой переменной. Ладно, прийдётся завернуть:

struct Instantiator
{
	Instantiator()
	{
		SomethingDoer<MyTypeList>::instantiate(); 
	}
};

Я не знаю, какой у Вас компилятор, но gcc-4.8.1 должен этот код скомпилировать. А в режиме отладки, возможно и скомпоновать. Но не более того. Что же происходит в производственном режиме(release)? Всё, что не используется по делу, будет выкинуто к отрубям собачим. А по делу не используется как раз самое важное, а именно: локальная переменная x из метода doSomething и конструктор класса Instantiator. Но, это не проблема. Нужно просто убедить компилятор, что эти две вещи всё же очень важны. С х – всё просто. Можно, к примеру, обьявить переменную volatile, и пусть только компилятор посмеет с ней что-нибудь сделать. А с конструктором Instantiator – возьмём и обьявим переменную типа Instantiator.
Вот как будет теперь выглядеть наш исходник:

#include "myclass.h"
#include "typelist.h"
#include <iostream>

namespace
{
template<typename Types>
struct SomethingDoer;

template<> struct SomethingDoer<EmptyList>
{
    static void doSomething(...)
    {}
};

template<typename Head, typename... Tail>
struct SomethingDoer<TypeList<Head, Tail...> >
{
    typedef void (MyClass::*MemFuncType)(Head);
    static void doSomething()
    {
      volatile MemFuncType x = &MyClass::f;
      (void)(x);
      SomethingDoer<typename TypeList<Head, Tail...>::tail_t>::doSomething();
    }
};

template <typename TList>
struct Instantiator
{
    Instantiator()
    {
        SomethingDoer<TList>::doSomething();
    }
};
Instantiator<MyTypeList> a; // приводит механизм инстанциирования в действие. 
}
template<typename T>
void MyClass::f(T x)
{
    std::cout<< x << "n";
}

Вот и всё, с задачей минимум мы уже справились. Шаблон определён в исходнике вместо заголовочного файла и инстанциирован для списка типов. Все изменения в необходимом списке типов будут автоматически отражаться на механизме инстанциирования, и нам не прийдётся прописывать явные инстанции шаблона вручную, если список типов изменится. В глаза бросается только один малюсенький недостаточек: весь этот код – это полнейшее уродство, которое вызовет у Ваших коллег и у Вас самих десяток “вотэфаков” и “чзхней”, когда Вы откроете его месяцок спустя.

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

// myclass.h
#pragma once
class MyClass
{
public:
    template<typename T>
    void f(T x);
};

//  myclass.cpp
#include "templateclass.h"
#include "typelist.h"
#include <iostream>

namespace
{
template<typename Types>
struct SomethingDoer;

template<> struct SomethingDoer<EmptyList>
{
    static void doSomething(...)
    {}
};

template<typename Head, typename... Tail>
struct SomethingDoer<TypeList<Head, Tail...> >
{
    typedef void (MyClass::*MemFuncType)(Head);
    static void doSomething()
    {
      volatile MemFuncType x = &MyClass::f;
      (void)(x);
      SomethingDoer<typename TypeList<Head, Tail...>::tail_t>::doSomething();
    }
};

template <typename TList>
struct Instantiator
{
    Instantiator()
    {
        SomethingDoer<TList>::doSomething();
    }
};
Instantiator<MyTypeList> a; // приводит механизм инстанциирования в действие. 
}
template<typename T>
void MyClass::f(T x)
{
    std::cout<< x << "n";
}
// typelists.h
#pragma once
struct EmptyList{};

template<typename Head, typename... Tail>
struct TypeList
{
    typedef Head head_t;
    typedef TypeList<Tail...> tail_t;
};

template<typename Head>
struct TypeList<Head, EmptyList>
{
    typedef Head head_t;
    typedef EmptyList tail_t;
};
typedef TypeList<int, double, bool, char, const char*, EmptyList> MyTypeList;

// main.cpp
#include <typelist.h>
#include "myclass.h"

int main()
{
    MyClass tc;
    tc.f(3);
    tc.f(2.0);
    tc.f(true);
    tc.f('a');
    tc.f("hello world");
    return 0;
}

Основным недостатком этого решения, помимо его уродства, является то, что оно частное. Мы инстанциировали шаблон одной конкретной функции одного конкретного класса. Для другого класса нам бы пришлось проделать всё тоже самое в другом исходнике. А если бы шаблонов было бы нескольно, то пришлось бы немного расширить метод doSomething, хотя это наименьшее из всех зол. Самым большим злом является то, что каждому пользователю прийдётся понимать, как это делается, ведь код механизма инстанциирования и код запуска этого механизма тесно переплетены между собой. Хорошо бы спрятать механизм, каким бы громоздким и запутанным он не был. Да так спрятать, чтобы пользователю было достаточно написать:

 InstantiateMemFunc(MyClass, f, MyTypeList) // (*)

Но об этом мы поговорим во второй части.

Автор: JoraN

Источник

Поделиться

* - обязательные к заполнению поля