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

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

    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<Какой-то тип, например, int>;  // Способ второй 
    

    О том какой способ нам лучше подойдёт сильно разсусоливать не буду, а то затянется надолго. Сразу скажу — второй подойдёт лучше.
    Раз Вы со мной согласны, то давайте попытаемся инстанциировать шаблон вторым способом.
    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) // (*)
    

    Но об этом мы поговорим во второй части.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 18

      +2
      О том, что такое список типов можно почитать здесь

      Я конечно не настаиваю, но может быть лучше дать ссылку на книгу «Современное проектирование на С++»?
      По куску исходного кода из Loki тяжеловато определить назначение этого класса.
        +2
        справедливо
        0
        Она, мне лично, больше нравится, так как позволяет писать
        typedef TypeList<int,char,bool,string, EmptyList> MyTypeList;

        Внесу и свой костыль:
        template<
        	typename T0 = internal::Void,
                // ...
        >
        struct make_typelist
        {
        	typedef List<T0, List<T1, List<T2, List<T3, List<T4, List<T5, List<T6, List<T7, List<T8, List<T9> > > > > > > > > > type;
        };
        typedef make_typelist<int, short, double, char>::type ...
        

        Так как мне не нравится указывать что-то EmptyList-а.
        А если задача состоит только в ограничении типов, которыми можно инстанциировать шаблон, то, как по мне, проще что-то такого:
        template<typename T>
        void foo(int x, typename std::enable_if</*Проверить, есть ли тип T в каком-то списке типов*/>::type* = NULL)
        {
        }
        
        template<typename T, typename = typename std::enable_if</**/>::type>
        struct Foo
        {
        };
        

        Это прекрасно работает и не приходится инстанциировать весь набор, а только то, что нужно
          0
          Внесу и свой костыль:

          Этот костыль можно было предвидеть. EmptyType был специально оставлен, чтобы приём с рекурсией был более понятен, если с ним кто незнаком.

          Но если уже задаваться целью от него избавиться, то тогда уже можно не стесняться:
          template <typename... Types> using NiceTypeList =  TypeList<Types..., EmptyList>;
          typedef NiceTypeList<int, double, bool, char, const char*> MyTypeList;
          
          


          А если задача состоит только в ограничении типов, которыми можно инстанциировать шаблон, то, как по мне, проще что-то такого:

          В том то и дело, что основная цель состоит не в ограничении типов, а в переносе определения шаблона в исходник.
          +2
          А зачем так много сложностей-то?
          // file: proto.h
          template<typename ... Types> struct TypeList {};
          
          using InstTypes = TypeList<int, float, double>;
          
          template<typename T> T foo();
          

          // file: proto.cpp
          template<typename T> T foo()
          {
          	return T(1);
          }
          
          template<typename T> struct Instantiator;
          
          template<> struct Instantiator<TypeList<>> {};
          
          template<typename T, typename ... Types> struct Instantiator<TypeList<T, Types...>>
          {
          	T (* const f)() = &foo<T>;
          	Instantiator<TypeList<Types...>> rest;
          };
          
          
          static Instantiator<InstTypes> i;
          

          // main.cpp
          #include <iostream>
          #include "proto.h"
          
          int main()
          {
              std::cout << foo<int>() << foo<float>() << foo<double>() << std::endl;
              return 0;
          }
          
            0
            Верно. Во второй части как раз нечто похожем и пойдёт речь. Я хотел показать весь мыслительный путь к окончательному решению.
              +2
              Этот путь кажется мне чересчур извилистым и тернистым. Какой смысл вообще откапывать TypeList в стиле Loki при наличии C++11? Если нужен просто список типов, то пользуемся непосредственно вариадиками, какие могут быть head-tail в наше тяжёлое время? Если нужна какая-то могучая работа над множествами типов, то зачем переизобретать boost::mpl?
            0
            Какая практическая задача решается «инстанциированием для определённого набора типов»?
              0
              Это, скорее не задача, а улучшение, если судьба шаблона — быть инстанциированным только для опеределённого набора типов. Улучшение заключается в переносе определения шаблона в исходник.
                0
                То есть практического применения у этой конструкции нет?
                  0
                  Иногда перенос инстанциаций в отдельный модуль трансляции позволяет очень сильно урезать размер кода (разумеется, за счет инлайнинга).
                0
                Ускорение компиляции, более «чистые» заголовочные файлы, возможность закрытия кода.
                +3
                Странная статья, variadic templates были созданы чтобы избавиться от списков типов, а вы их конвертируете в список типов, зачем? При этом работа с вариадиками куда как проще чем со списком типов. По сути чтобы сохранить вариадик, его надо упаковать либо в пустой тип, либо как вариант в тип функции.

                template<typename ... TArgs> struct holder; template<typename ... TArgs> struct vpack { typedef void(functtion_type)(TArgs...); typedef holder<TArgs...> holder_type; };
                Ну и распаковать их очень легко

                template<typename T> struct funpack; template<typename ... TArgs> struct funpack<void(TArgs...)> { } template<typename T> struct hunpack; template<typename ... TArgs> struct hunpack<holder<TArgs...>> { }

                Ну и есть еще std::tuple. Попробуйте избавиться от списков типов и сразу заметите как упростится код, да и увеличится скорость компиляции. Ну и по сути если хотите работать с вариадиками, ни в коем случае не отталкивайтесь от книги Александреску, в этом случае увы, она только вредна, ибо механизмы работы другие. Можете поглядеть старую версию моей либы github.com/axispod/vmpl, увы заточена пока под VS, не хватает времени довести до ума. Есть простенькие алгоритмы для работы с вариадиками.
                  0
                  Забыл сразу сказать, что внутри возможны алгоритмы схожие по работе со списками типов, но это требуется совсем не всегда. Например это требуется при работае с элементом в определенной позиции, например разрезать вариадик на 2 отдельных куска по определенной позиции.
                    0
                    Если бы знал, что последуют комментарии, почему списки типов вместо вариадиков и т.д, то сразу бы написал с вариадиками. Именно с ними и будет показано решение во второй части. Здесь речь идёт о том, как добиться переноса определения шаблона в исходный файл. А какие инструменты для этого используются — это уже вторично.
                    Глянул Вашу либу, с map — интерессная штука. Где-то применяли?
                      +3
                      Проблема не в том, что у вас списки типов вместо вариадиков. У вас списки типов поверх вариадиков. И вот от этого уже сносит крышу окончательно. Если вы такой бедный несчастный, что вам нужно поддерживать какой-нибудь проект для MSVC 2010 и вариадиков у вас просто нету — ну Ok, вам можно посочувствовать, но да, придётся мучиться. Но если вы уже начали использовать варидики, то, я извиняюсь, зачем поверх них-то списки типов городить? Где-то мы это уже видели:
                      Скрытый текст
                      image
                        0
                        map хотел применять, но в последствии придумал более оптимальный механизм и пока отказался от дальнейшего развития. В основном vmpl дописываю только по мере необходимости.
                    0
                    так как позволяет писать
                    typedef TypeList<int,char,bool,string, EmptyList> MyTypeList;
                    вместо классической записи
                    typedef TypeList<int,TypeList<char,TypeList<bool,TypeList<string, EmptyList>>>> MyTypeList;

                    Мне не приходилось пользоваться Loki (предпочитаю boost::mpl), но, тем не менее, я заглядывал в исходники из любопытства. Там есть мейкеры для TypeList:
                    typedef MakeTypelist<int,char,bool,string>::Result MyTypeList;

                    Также на макросах:
                    typedef LOKI_TYPELIST_4(int,char,bool,string) MyTypeList;

                    Only users with full accounts can post comments. Log in, please.