Variadic templates в C++0x

    Те, кто читал книгу Андрея Александреску «Современное программирование на C++» знают, что существует обширный класс задач (в области метапрограммирования с использованием шаблонов), когда шаблону при инстанцировании необходимо указать переменное (заранее неизвестное) количество аргументов. Типичные примеры таких задач:
    — Описание кортежей (tuples)
    — Описание типов наподобие вариантов (variants)
    — Описание функторов (в этом случае перечень типов аргументов зависит от сигнатуры функции)
    — Классификация типов по заранее заданным множествам
    — и т. п.

    В каждой такой задаче точное количество типов, передаваемых соответствующему шаблону в качестве аргументов, заранее определить сложно. И, вообще говоря, зависит от желания и потребностей того, кто намеревается использовать соответствующий шаблонный класс.
    В рамках действующего стандарта С++ сколь-нибудь удобного решения таких задач не существует. Шаблоны могут принимать строго определённое количество параметров и никак иначе. А. Александреску (в упомянутой выше книге) предлагает общее решение, основанное на т. н. «списках типов», в котором типы представлены в виде односвязного списка, реализованного посредством рекурсивных шаблонов. Альтернативным решением (используемом, например, в boost::variant и boost::tuple) является объявление шаблонного класса с большим количеством параметров, которым (всем, кроме первого) присвоено некоторое значение по умолчанию. Оба этих решения являются половинчатыми и не охватывают весь спектр возможных задач. По этому, для устранения недостатков существующих решений и упрощения кода новый стандарт предлагает С++-разработчикам новый вариант объявления шаблонов? «шаблоны с переменным количеством параметров» или, в оригинале, «variadic templates».


    Простые варианты использования


    Объявление шаблона с переменным количеством параметров выглядит следующим образом:
    template<typename ... Types>
    class VariadicTemplate
    {
    };

    подобным же образом объявляются шаблоны с переменным количеством параметров-не типов:
    template<int ... Ints>
    class VariadicTemplate
    {
    };

    Здесь необходимо отметить, что эмуляция подобного в рамках текущего стандарта — весьма нетривиальная задача (если не сказать, что невыполнимая).
    Помимо шаблонных классов, можно также объявлять шаблонные функции с переменным количеством параметров. Подобные объявления могут выглядеть следующим образом:
    template<typename ... Type>
    void printf(const char* format, Type ... args);

    Очевидно, что такого рода параметры шаблонов (они называются «пакетами параметров» или «parameters packs») не могут использоваться везде, где могут использоваться обычные (одиночные) параметры шаблонов. Допустимо использование пакетов параметров в следующих контекстах:
    • В перечислении базовых классов шаблона (base-specifier-list);
    • В списке инициализации членов данных в конструкторе (mem-initializer-list);
    • В списках инициализации (initializer-list)
    • В списках параметров других шаблонов (template-argument-list);
    • В спецификации исключений (dynamic-exception-specification);
    • В списке атрибутов (attribute-list);
    • В списке захвата лямбда-функции (capture-list).

    В зависимости от того, где именно используется пакет параметров, соответствующим образом интерпретируются элементы этого пакета. Само использование пакета параметров называется «раскрытием пакета» (pack expansion), и записывается в коде следующим образом:

    Types ...

    Где Types — это название пакета параметров.
    Например, для такого объявления шаблона:
    template<typename ... Types>
    class VariadicTemplate
    {
    };

    возможные варианты раскрытия пакета параметров могут выглядеть так:
    class VariadicTemplate : public Types ... // раскрытие в список базовых классов. 'public Types' - паттерн
    {
    //…
        // Раскрытие в список параметров другого шаблона. Паттерн - Types
        typedef OtherVariadicTemplate<Types ...> OtherVT;
        // Более сложный вариант. Паттерн - Types *
        typedef OtherVariadicTemplate<Types* ...> SomeOtherVT;
        // Раскрытие в список параметров функции. Паттерном является Types, a args - это новый список параметров:
        void operator () (Types ... arg)
        {
            // Раскрытие в список аргументов при вызове функции
            foo(&args ...);
        }
        // Раскрытие в списке инициализации в конструкторе:
        VariadicTemplate() : Types() …
    };

    Под термином «паттерн» здесь понимается фрагмент кода, окружающего имя пакета параметров, который будет повторяться при раскрытии соответствующего пакета. В приведённом примере, если провести раскрытие параметров в ручную, то получится, что такое инстанцирование шаблона:
    /* ... */ VariadicTemplate<intchardouble/* ... */

    Будет раскрыто следующим образом:
    class VariadicTemplate : public intpublic charpublic double
    {
    //…
        typedef OtherVariadicTemplate<intchardouble> OtherVT;
        typedef OtherVariadicTemplate<int*, char*, double*> SomeOtherVT;
        void operator () (int args1, char args2, double args3)
        {
            foo(&args1, &args2, &args3);
        }
        VariadicTemplate() : int(), char(), double() // очевидно, этот код получится некомпилируемый для такого списка типов
    };

    В качестве достаточно простого примера использования шаблонов с переменным числом параметров можно привести реализацию функтора. Выглядит эта реализация следующим образом:
    #include <iostream>

    // Объявляем общий вариант шаблона, хранящего указатель на функцию. При этом все возможные типы, которые могут придти в шаблон
    // в процесс инстанцирования, мы упаковываем в пакет параметров
    template<typename ... Args> struct FunctorImpl;

    // Специализируем шаблон для указателя на простые функции. При этом указываем, что пакет параметров содержит тип возвращаемого
    // значения ( R ) и аргументы (Args). Из этих двух параметров (простого, и пакетного) затем формируем сигнатуру функции
    template<typename R, typename ... Args>
    struct FunctorImpl<R (Args ...)>
    {
        // Описываем тип указателя на функцию с нужной сигнатурой. При этом раскрываем пакет параметров
        typedef R (*FT)(Args ...);
        
        FunctorImpl(FT fn) : m_fn(fn) {;}
        
        // Объявляем оператор вызова функции таким образом, что он принимает на вход ровно столько параметров, сколько аргументов
        // у хранимого типа функции.
        R operator () (Args ... args)
        {
            // Вызываем функцию передавая ей все полученные аргументы
            return m_fn(args ...);
        }
        
        FT m_fn;
    };

    // Объявляем общий шаблон-диспетчер
    template<typename FT>
    struct Functor : public FunctorImpl<FT>
    {
        Functor() : FunctorImpl<FT>(NULL) {;}
        Functor(FT fn) : FunctorImpl<FT>(fn) {;}
    };

    int plus_fn(int a, int b) {return a + b;}
    int minus_fn(int a, int b) {return a - b;}
    int increment(int& a) {return a ++;}

    int main()
    {
        Functor<int (intint)> plus(plus_fn);
        Functor<int (intint)> minus(minus_fn);
        Functor<int (int&)> inc(increment);

        std::cout << plus(1020) << " " << minus(1020) << std::endl;

        int a = 100;
        std::cout << inc(a) << " ";
        std::cout << a << std::endl;
    }

    Результат выполнения этого кода вполне ожидаемый:

    30 -10
    100 101

    а код — простой и понятный. Для сравнения можно посмотреть файлы с реализацией boost::function.
    Описанные выше шаблоны несложно специализировать для указателей на функции-члены:
    // Объявляем специализацию контейнера функции для указателя на функцию член, конкретезируя всё тот же пакет параметров
    template<typename T, typename R, typename ... Args>
    struct FunctorImpl<R (T::*)(Args ...)>
    {
        typedef R (T::*FT)(Args ...);
        typedef T HostType;
        
        FunctorImpl(FT fn = NULL, T* obj = NULL) : m_fn(fn), m_obj(obj) {;}
        
        // Объявляем два варианта оператора вызова функции - для случая, когда функтор используется как «замыкание», и когда объект,
        // для которого вызывается метод, передаётся первым аргументом
        R operator() (Args... args)
        {
            (m_obj->*m_fn)(args ...);
        }
        
        R operator() (T* obj, Args... args)
        {
            (obj->*m_fn)(args ...);
        }
        
        FT m_fn;
        T* m_obj;
    };


    // Объявляем класс-замыкание, принимающий в конструкторе объект, для которого будет вызываться функция-член. Выглядит он очень просто
    template<typename FT>
    struct Closure : public FunctorImpl<FT>
    {
        typedef typename FunctorImpl<FT>::HostType HostType;
        Closure(HostType* obj, FT fn) : FunctorImpl<FT>(fn, obj) {;}
    };

    // Использование
    class A
    {
    public:
        A(int base = 0) : m_base(base) {;}
        int foo(int a) {return a + m_base;}
        
    private:
        int m_base;
    };

    A b1(10), b2;
    Closure<int (A::*)(int)> a_foo(&b1, &A::foo);
    // Можно заметить, что общаяя реализация функтора также корректно работает с указателями на функции-члены
    Functor<int (A::*)(int)> b_foo(&A::foo);

    std::cout << a_foo(20) << " " << b_foo(&b2, 20) << " " << b_foo(&b1, 20) << std::endl;

    Приведённый пример достаточно прост и наглядно демонстрирует основные возможности шаблонов с переменным количеством параметров. Анализируя его можно определить следующую общую схему использования шаблонов с переменным количеством параметров:
    1. Декларируется наиболее общий шаблон, последний параметр которого описывается в виде пакета параметров. В примере это
    template<typename ... Args> struct FunctorImpl;

    2. Определяются частичные специализации этого шаблона, конкретизирующие ту или иную часть пакета параметров. В приведённом примере это определение
    template<typename R, typename ... Args>
        struct FunctorImpl<R (Args ...)>

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

    Более сложные случаи


    Как отмечалось выше, пакеты параметров могут содержать не только типы, но и не-типы. Например:
    // Объявляем шаблон, принимающий переменное количество целых числе
    template<int ... Nums>
    struct NumsPack
    {
        // Объявляем статический массив, размер которого равен количеству фактически переданных аргументов
        static int m_nums[sizeof...(Nums)];
        // А также объявляем перечисление, сохраняющее количество элементов в массиве
        enum {nums_count = sizeof ... (Nums)};
    };

    // Инициализируем статический массив
    template<int ... Nums>
    int NumsPack<Nums ...>::m_nums[] = {Nums ...};
    Проверочный код:
    typedef NumsPack<1020304050> Nums_5;
    std::cout << Nums_5::nums_count << std::endl;
    for (int n = 0; n < Nums_5::nums_count; ++ n)
        std::cout << Nums_5::m_nums[n] << " ";
    std::cout << std::endl;

    печатает на консоль ожидаемые
    5
    10 20 30 40 50

    Конструкция sizeof … (Nums), приведённая в этом примере, используется для получения количества параметров в пакете. В ней Nums — это имя пакета параметров. К сожалению, дизайн шаблонов с переменным количеством параметров таков, что это — единственное, что можно сделать с пакетом параметров (помимо его непосредственно раскрытия). Получить параметр из пакета по его индексу, например, или совершить какие-либо более сложные манипуляции в рамках проекта нового стандарта невозможно.

    При раскрытии пакетов можно применять более сложные паттерны. Например, в приведённом выше коде можно сделать следующую замену:
    template<int ... Nums>
    int NumsPack<Nums ...>::m_nums[] = {Nums * 10  ...};

    что приведёт к выводу на экран другой последовательности:
    100 200 300 400 500

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

    Такая ситуация может возникать в случае, когда требуется определить кортежи значений. Предположим, необходимо организовать универсальный функтор-композитор, задача которого — передать в некоторую функцию результаты выполнения заданных функций для некоего аргумента. Пусть существует некоторый набор функций:
    double fn1(double a)
    {
        return a * 2;
    }

    int fn2(int a)
    {
        return a * 3;
    }

    int fn3(int a)
    {
        return a * 4;
    }

    И две операции:
    int test_opr(int a, int b)
    {
        return a + b;
    }

    int test_opr3(int a, int b, int c)
    {
        return a + b * c;
    }

    Необходимо написать универсальный функтор, применение операции вызова функции к которому приводило бы к выполнению такого кода:
    test_opr(f1(x), f2(x));
    или
    test_opr3(f1(x), f2(x), f3(x));

    Функтор должен принимать на вход операцию и перечень функций, результаты работы которых надо передать в качестве аргументов этой операции. Каркас определения такого функтора может выглядеть следующим образом:
    template<typename Op, typename … F>
    class Compositor
    {
    public:
        Compositor(Op op, F … fs);
    };

    Первая проблема, которую необходимо решить — это каким образом сохранять переданные функции. Для этого можно применить множественное наследование от классов, непосредственно хранящие данные заданного типа:
    template<typename T>
    struct DataHolder
    {
        T m_data;
    };

    template<typename Op, typename … F>
    class Composer : public DataHolder<F> …
    {
        // ...
    };

    Но тут возникает первая проблема — если в списке передаваемых функций присутствуют несколько функций, типы которых совпадают, то код не скомпилируется, т. к. в списке базовых классов будет присутствовать один и тот же класс. Для устранения этой неоднозначности типы в пакете можно проиндексировать. Для этого будет использоваться вспомогательный тип «кортеж целых чисел», содержащий числа от 0 до заданного в качестве параметра N:
    // Определяем класс собственно кортежа
    template<int ... Idxs> struct IndexesTuple 
    {
    };

    // Определяем общий вид шаблона, используемого для порождения кортежа
    template<int Num, typename Tp = IndexesTuple<>>
    struct IndexTupleBuilder;

    // Определяем специализацию, которая генерирует последовательность чисел в виде пакета целочисленных параметров.
    // Для этого в качестве второго параметра в объявлении шаблона используется не собственно тип кортежа, а ранее сформированный
    // пакет. Для получения итогового пакета наследуемся от порождающегося шаблона, добавляя в пакет новое число
    template<int Num, int ... Idxs> 
    struct IndexTupleBuilder<Num, IndexesTuple<Idxs ...>> : IndexTupleBuilder<Num - 1, IndexesTuple<Idxs ..., sizeof ... (Idxs)>> 
    {
    };

    // Терминирующая рекурсию специализация. Содержит итоговый typedef, определяющий кортеж с нужным набором чисел
    template<int ... Idxs>
    struct IndexTupleBuilder<0, IndexesTuple<Idxs ...>>
    {
        typedef IndexesTuple<Idxs...> Indexes;
    };

    В итоге, использовать этот шаблон можно следующим образом:
    typedef typename IndexTupleBuilder<6> Indexes;
    При этом Indexes будет эквивалентно IndexesTuple<0, 1, 2, 3, 4, 5>

    Для того, чтобы этот класс был применён в реализации композитора, надо ввести промежуточный базовый класс, который и будет наследовать от классов с данными. При этом каждый класс с данными будет снабжён своим уникальным индексом:
    template<int idx, typename T>
    struct DataHolder
    {
        DataHolder(T const& data) : m_data(data) {;}
        
        T m_data;
    };

    // Сначала объявляем общий шаблон, принимающий на вход кортеж. Объявление непосредственно в таком виде нам не потребуется, но
    // оно требуется для последующей специализации.
    template<typename IdxsTuple, typename ... F> struct ComposerBase;

     // Специализируем общий шаблон, извлекая из кортежа пакет параметров. 
    // В данном случае шаблон объявляется с двумя пакетами параметров. Это разрешено, т. к. пакеты могут быть однозначно разделены
    // При наследовании используется паттерн, в котором упоминается сразу два пакета параметров. Это позволяет однозначно сопоставить
    // элементы целочисленного кортежа и перечня типов функций.
    template<int ... Idxs, typename ... F>
    struct ComposerBase<IndexesTuple<Idxs...>, F ...> : public DataHolder<Idxs, F>... 
    {
        // А здесь паттерн содержит сразу три пакета - пакет с индексами, пакет типов функций и пакет аргументов. Всё это раскрывается в список
        // инициализации конструктора.
        ComposerBase(F ... fs) : DataHolder<Idxs, F>(fs)... {;}
    };

    // Наследуем шаблон композитора от описанного выше шаблона, содержащего фактические данные
    template<typename Op, typename ... F>
    struct Composer : public ComposerBase<typename IndexTupleBuilder<sizeof...(F)>::Indexes, F...>
    {
        Op m_op;
    public:
        // Объявляем конструктор 
        Composer(Op op, F const &... fs) : m_op(op), Base(fs...) {;}    
    };

    Для того, чтобы завершить реализацию композитора, необходимо определить оператор вызова функции. Для удобства его определения сначала определяется тип возвращаемого значения:
    template<typename Op, typename ... F>
    struct Composer : /* …  */
    {
        Op m_op;
    public:
        typedef decltype(m_op((*(F*)NULL)(0)...)) result_t;
        // ...
    };

    Для определения типа возвращаемого значения используется другая новая для C++ конструкция — decltype. Результатом её применения (в данном случае) является тип возвращаемого функцией значения. Конструкция выглядит несколько странной. По смыслу она эквивалентна такой
    decltype(op(fs(0) …))
    Но поскольку в области видимости класса пакет fs не определён, то оператор применяется к сконвертированному к ссылке на тип функции NULL.

    Теперь всё готово для определения оператора вызова функции. Поскольку классы, хранящие участвующие в композиции функции, в качестве одного из параметров шаблона принимают целочисленный индекс, то этот оператор реализуется через вспомогательную функцию, в которую передаётся всё тот же целочисленный кортеж:
    template<typename Op, typename ... F>
    struct Composer : /* …  */
    {
        Op m_op;
    public:   
        ret_type operator()(int x) const 
        {
            return MakeCall(x, Indexes());
        }
    private:
        // Здесь используется тот же самый трюк, что и в определении класса ComposerBase. Тип кортежа используется для того, чтобы «поймать»
        // пакет целочисленных индексов
        template<int ... Idxs>
        ret_type MakeCall(int x, IndexesTuple<Idxs...> const&) const 
        {
            return m_op(DataHolder<Idxs, F>::m_data(x)...);
        }
    };
    Осталось только определить функцию, облегчающую создание экземпляров этого класса:
    template<typename Op, typename ... F>
    Composer<Op, F ...> Compose(Op op, F ... fs)
    {
        return Composer<Op, F...>(op, fs ...);
    }

    и композитор готов. Пара примеров его использования:
    auto f = MakeOp(test_opr, fn1, fn2);
    auto ff = MakeOp(test_opr3, fn1, fn2, fn3);
    auto ff1 = MakeOp(test_opr3, fn1, fn2, [=](int x) {return f(x) * 5;}); // здесь последним параметром в композитор передаётся лямбда-функция.

    Полное определение шаблонного класса-композитора выглядит следующим образом:
    template<int ... Idxs, typename ... F>
    struct ComposerBase<IndexesTuple<Idxs...>, F ...> : public DataHolder<Idxs, F>... 
    {
        ComposerBase(F ... fs) : DataHolder<Idxs, F>(fs)... {;}
    };


    template<typename Op, typename ... F>
    struct Composer : public ComposerBase<typename IndexTupleBuilder<sizeof...(F)>::Indexes, F...>
    {
        Op m_op;
    public:
        typedef ComposerBase<typename IndexTupleBuilder<sizeof...(F)>::Indexes, F...> Base;
        typedef decltype(m_op((*(F*)NULL)(0)...)) result_t;

        Composer(Op op, F const &... fs) : m_op(op), Base(fs...) {;}

        result_t operator()(int x) const 
        {
            return MakeCall(x, typename IndexTupleBuilder<sizeof...(F)>::Indexes());
        }
    private:

        template<int ... Idxs>
        result_t MakeCall(int x, IndexesTuple<Idxs...> const&) const 
        {
            return m_op(DataHolder<Idxs, F>::m_data(x)...);
        }
    };

    Также этот класс можно было бы реализовать на базе кортежей из STL (std::tuple). В этом случае в классе DataHolder не было бы необходимости. В этом случае реализация композитора будет следующей:
    template<typename Op, typename ... F>
    class TupleComposer
    {
        Op m_op;
        std::tuple<F ...> m_fs;
    public:
        typedef decltype(m_op((*(F*)NULL)(0)...)) result_t;
        
        TupleComposer(Op op, F... fs) : m_op(op), m_fs(fs ...) {;}

        result_t operator()(int x) const 
        {
            return MakeCall(x, typename IndexTupleBuilder<sizeof...(F)>::Indexes());
        }
    private:

        template<int ... Idxs>
        result_t MakeCall(int x, IndexesTuple<Idxs...> const&) const 
        {
            return m_op(std::get<Idxs>(m_fs)
    (x)...);
        }
    };

    Такой вариант выглядит несколько проще.

    Ещё некоторое количество хитростей


    Раскрытие пакета параметров в контексте «список инициализации» предоставляет программисту достаточно большую свободу действий, т. к. в этом случае паттерном может быть полноценное выражение. Например, сумму переданных в качестве аргументов чисел можно посчитать так:
    template<typename… T>
    void ignore(T ...) {;}
     
    template<typename… T>
    int CalcSum(T… nums)
    {
         int ret_val = 0;
         ignore(ret_val += nums ...);
         return ret_val;
    }

    Проверить, есть ли среди переданных чисел положительные — так:
    template<typename… T>
    bool HasPositives(T… nums)
    {
        bool ret_val = true;
        ignore(ret_val = ret_val && nums >= 0 ...);
       return ret_val;
    }

    Но при использовании такого метода нельзя забывать, что последовательность вычислений аргументов, строго говоря, не определена, и в каком именно порядке будут выполнены операции — заранее сказать нельзя.

    Подводя итог, можно сказать, что шаблоны с переменным количеством параметров — очень мощное средство, появляющееся в языке C++. Они лишены очевидных недостатков существующих сейчас списков типов (или иных эмуляций подобного поведения), позволяют относительно небольшим объёмом кода выражать достаточно сложные концепции. Приведённые в этой статье конструкции можно сравнить с аналогичными, выполненными в рамках действующего стандарта (для этого можно заглянуть в исходные файлы boost::bind, boost::function, boost::tuple). Но они не лишены и некоторых недостатков. Главный из них — ограниченное число контекстов, в которых пакеты параметров могут раскрываться. В частности, пакеты не могут раскрываться внутри лямбда-функций (соответствующий запрос направлен в комитет по стандартизации, но будет ли этот запрос удовлетворён?), пакеты не могут раскрываться в выражения, чтобы можно было написать, например, так:
    auto result = args + ...;
    к элементам пакета нельзя обращаться по индексу.
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 16
    • +1
      Отличная статья по достаточно сложной теме, спасибо!
      Но есть несколько замечаний:
      1. Замените NULL на nullptr(раз уж С++0x так пусть будет везде)
      2. Какая-то проблема с комментариями, Ю они обрезаны в коде. Т.е. комментарии больше определенной длины не закончены
      3. Добавьте где это можно собрать(если можно где-то)
      • 0
        В плане сборки не знаю точно, надо глядеть gcc с девелоперской ветки.
        • +1
          Спасибо. :)

          1 — если пофиксить, то пример не будет собираться ни одним из отрелизиных в настоящий момент компиляторов.
          2 — пофикшу.
          3 — собрать в полном объёме (вместе с лямбдами) можно с помощью gcc 4.5 и gcc 4.6 (который сейчас только разрабатывается). Без лямбд — начиная с gcc 4.3. Собственно, NULL в коде появился из за того, что nullptr в gcc 4.5 не поддерживается (а вот в 4.6 уже есть).
          • 0
            #define nullptr NULL
            :)
        • 0
          Оказывается приятные вещи появились в новом стандарте С++. Спасибо за статью.
          И спасибо Александреску. Его труды не пропали даром.
          • 0
            Да, вещи появились действительно приятные. К сожалению (по крайней мере моему) некоторые из них не в достаточной степени доведены до кондиции.
          • 0
            Полезная вещь действительно. Сам столкнулся с такой необходимостью недавно. Использовал TYPE_LIST из библиотеки Александреску.
            • +1
              «При раскрытии пакетов можно применять более сложные паттерны. Например, в приведённом выше коде можно сделать следующую замену:
              template<int… Nums>
              int NumsPack<Nums ...>::m_nums[] = {Nums * 10 ...};

              что приведёт к выводу на экран другой последовательности:
              10 20 30 40 50»

              — ? 100 200 300 400 500

              мне понравилась, хорошая лаконичная статья!
              • 0
                Вы, конечно же, правы. У меня в тексте опечатка.
              • 0
                А можно ли реализовать делегат с использованием varadic templates, чтобы явно не указывать типы в угловых скобках при инициализации объекта?
                • 0
                  В смысле? Не совсем понял суть вопроса.
                  • 0
                    Чтобы в коде можно было писать:

                    class A
                    {
                    public:
                    void f1(int){};
                    void f2(int, int){};
                    };

                    A oA;

                    Delegate oDelegate1(&oA, &A::f1);
                    Delegate oDelegate2(&oA, &A::f2);

                    а не

                    Delegate<void (A::*)(int)> oDelegate1(&oA, &A::f1);
                    Delegate<void (A::*)(int, int)> oDelegate2(&oA, &A::f2);

                    грубо говоря, чтобы типы выводились из сигнатуры вызова конструктора.
                    В статье, на которую я ссылался описывается способ, с использованием интерфейса, там есть потеря производительности, связанная с виртуальным вызовом, но и по аналогии я не смог реализовать на varadic templates. Для меня открытием в этой статье стало использования вот такой формы unpacking — class Composer: public DataHolder<_ZZ> …, знал только о такой class Composer: public _ZZ …, плюс как сгенирировать последовательность чисел, попробую реализовать delegat с новыми способами понимания varadic templates.Еще раз спасибо за статью, очень классная!
                    • 0
                      Теперь понятно. Но ведь никто не мешает вам использовать следующие методы:
                      1. auto:
                      auto oDelegate1 = MakeDelegate(&oA, &A::f1);
                      auto oDelegate2 = MakeDelegate(&oA, &A::f2);
                      2. decltype:
                      decltype(MakeDelegate(nullptr, &A::f1)) oDelegate1;
                      decltype(MakeDelegate(nullptr, &A::f2)) oDelegate1;

                      Второй вариант можно использовать при объявлении членов класса. Вот как-то так.
                      • 0
                        Спасибо большое, вы задействовали многое из новых возможностей С++, я тут эксперементирую на Visual Studio 2012 November CP100, вот пока и пытаюсь ограничивать себя в возможностях, например ваш код из второй части статьи не смог там скомпилироваться, вот эта строчка ему не нравится: typedef decltype(m_op((*(F*)NULL)(0)...)) result_t; Но у меня будет возможность скоро и с gcc поработать.Спасибо еще раз за пищу для размышления!
                        • 0
                          Собственно, в этом заключается основная причина, по которой я практически перестал использовать MSVC в своих проектах. Для таких экспериментов gcc и clang подходят куда как лучше. :) Сейчас использую связку QtCreator + mingw gcc. Удачи вам в ваших экспериментах!
                • 0
                  «и композитор готов. Пара примеров его использования:
                  auto f = MakeOp(test_opr, fn1, fn2);
                  auto ff = MakeOp(test_opr3, fn1, fn2, fn3);
                  auto ff1 = MakeOp(test_opr3, fn1, fn2, [=](int x) {return f(x) * 5;}); // здесь последним параметром в композитор передаётся лямбда-функция.»
                  вместо MakeOp->Composer

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.