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

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

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

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

template <typename... Types> struct TypeList{}; // пустой класс для упаковки наборов типов
typedef TypeList<int, double, bool, char, const char*> MyTypeList; // конкретный набор типов для инстанциирования шаблона функции 

Вернёмся к последнему результату из первой части и перепишем его на манер вариадиков:

template<typename T> struct SomethingDoer
{
    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<TypeList<Tail...>>::doSomething();
    }
};
template <typename TList>
struct Instantiator
{
    Instantiator()
    {
        SomethingDoer<TList>::doSomething();
    }
};
Instantiator<MyTypeList> a;

Теперь давайте подумаем, как усовершенствовать это решение. Совершенствование необходимо в двух направлениях: обобщение и избавление от хлама.
Со вторым заданием, вроде проще, разобраться. Весь класс Instantiator только и создан для того, чтобы слелать вызов: SomethingDoer<MyTypeList>::doSomething();. Ну, так это можно сделать и проще:

template<typename T> struct SomethingDoer
{
    static int doSomething() // теперь doSomething возвращает значение, а это значит, что её результат можно присвоить переменной. 
    {
        return 0;
    }
};
template<typename Head, typename... Tail>
struct SomethingDoer<TypeList<Head, Tail...>>
{
    typedef void (MyClass::*MemFuncType)(Head);
    static int doSomething()
    {
        volatile MemFuncType x = &MyClass::f; (***)
        (void)(x);
        return SomethingDoer<TypeList<Tail...>>::doSomething();
    }
};
int a = SomethingDoer<MyTypeList>::doSomething(); // тоже самое, что и раньше, только немного короче. 

Ну, а теперь давайте обобщать. Самое больное место отмечено (***). Здесь происходит инстанциирования конкретного шаблона метода конкретного класса.
Весь остальной код ни с этим шаблоном, ни с классом ничего общего не имеет. Значит нужно вынести остаток кода за скобки, а в строчку с (***) вставить что-то более обобщённое. Т.е, если мы засунем шаблон MyClass::f в какую-нибудь оболочку, типа

template<typename T>        
struct FuncWrapper_MyClass_f
{
  typedef decltype(&MyClass::f<T>) type; 
  static constexpr type value = &MyClass::f<T>; 
}; 

, то внутри функции doSomething нам уже ни класс MyClass, ни его шаблон метода f() не понадобятся.
Вот как теперь можно записать (класс SomethingDoer теперь переименован в Instantiator, а метод doSomething() — в instantiate()).

template<typename Head, typename... Tail> // пока всё, как и раньше
struct Instantiator<TypeList<Head, Tail...>> // пока всё, как и раньше
{
  typedef Instantiator<TypeList<Tail...> > TailInstantiator; // тип для рекурсивного инстанциатора для хвоста списка типов
  static std::pair<typename FuncWrapper_MyClass_f<Head>::type, decltype(TailInstantiator::instantiate())> instantiate()
  {
        auto headTypeInstantiate = FuncWrapper_MyClass_f<Head>::value; // здесь шаблон инстанциируится для головы списка,
        return std::make_pair(headTypeInstantiate, TailInstantiator::instantiate()); // а здесь рекурсивно запускается инстанциирование для хвоста списка. 
  }
};

Не обращайте внимания на всякие навороты с возвратом пары — это нужно, чтобы компилятор не попытался отлынить от части своей работы. Теперь осталось только заменить упаковочный класс FuncWrapper_MyClass_f на что-то более общее. Всё, что нам нужно внутри метода instantiate() — это аттрибут FuncWrapper_MyClass_f::value и тип FuncWrapper_MyClass_f::type. Выглядеть это будет так:

template<template<typename>class FuncWrapperType, typename Type> // первый параметр шаблона - и есть тот новый обобщённый FuncWrapper_MyClass_f
struct Instantiator
{
  static int instantiate(...)
  {
    return 0;
  }
};

template<template<typename>class FuncWrapperType, typename Head, typename... Tail>
struct Instantiator<FuncWrapperType, TypeList<Head, Tail...> >
{
  typedef Instantiator<FuncWrapperType, TypeList<Tail...> > TailInstantiator;
  static typename std::pair<typename FuncWrapperType<Head>::type, decltype(TailInstantiator::instantiate())> 
  instantiate()
  {
    auto headTypeInstantiate = FuncWrapperType<Head>::value;
    return std::make_pair(headTypeInstantiate, TailInstantiator::instantiate());
  }
};
auto a = Instantiator<FuncWrapper_MyClass_f, MyTypeList>::instantiate(); // всё, как раньше, только теперь указываем FuncWrapper_MyClass_f снаружи instantiate(), а не внутри. 

Данный шаг был самым важным из всех предыдущих. Нам удалось сделать механизм инстанциирования независимым от конкретных шаблонов, для которых этот механизм должен быть применён. Весь код struct Instantiator стал общим и теперь может быть вынесен из исходника myclass.cpp и быть использован для инстанциирования шаблонов других функций или методов класса. Всё что остаётся прописать в MyClass.cpp — это упаковочку FuncWrapper_MyClass_f. Но и это дело неблагородное, и если у Вас нету религиозных соображений на тему макросов препроцессора, то давайте для чёрной работёнки ими и воспользуемся.

define InstantiateMemFunc(className, funcName, types) 
template<typename T>        
struct FuncWrapper_##className_##funcName 
{ 
  typedef decltype(&className::funcName<T>) type; 
  static constexpr type value = &className::funcName<T>; 
}; 
volatile auto i_##className_##funcName = detail::Instantiator<FuncWrapper_##className_##funcName, types>::instantiate();

Этот код тоже универсален и может быть помещён в центральном месте. В MyClass.cpp остаётся лишь написать:

namespace {
 InstantiateMemFunc(MyClass, f, MyTypeList)
}

И всё. Также можно написать очень похожий макрос для свободных функций, а не методов класса, но не хочется место переводить.

Подведём итоги. В этой статье мы рассмотрели, как можно избавиться от определеия шаблонов в заголовочном файле при определённых условиях. Не ахти какая проблема, конечно, но всё-таки не аккуратненько. Заголовочный файл — это лицо программиста, а исходник — это его опа. На опе волосы могут быть, а на лице, кроме админов, раввинов и, разумеется, админ-раввинов, — нет. Мы, С++ программисты, должны следить за своим лицом, иначе оно превратится в…

Автор: JoraN

Источник

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js