среда, 19 мая 2010 г.

Анатомия boost::bind

Не знаю, является эта тема моветоном или нет, только что "на спор" написал свой набросок реализации...

Итак, что у нас в пререквизитах:
  • Хочется обеспечить функциональность, близкую к типовым случаям использования boost::bind
  • Нужно обеспечить четкое понимание того, как это работает у оппонента
  • Работать будем в среде MSVC 2003 (вот такой вот я ретроград)

    Типовые примеры, которые должны обслуживаться:
    #include <vector>
    #include <iostream>
    #include <functional>
    #include <string>

    #include "mbind.h"

    int f4(int a, int b, int c, int d)
    {
    return a + b + c + d;
    }

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

    int f2(int a, int b)
    {
    return a + b;
    }

    int f1(int a)
    {
    return a + 1;
    }

    int f0()
    {
    return 1;
    }

    struct X
    {
    typedef int result_type;
    int f1(int a)
    {
    return a+1;
    }

    int f0()
    {
    return 1;
    }

    int f(int a1, int a2, int a3)
    {
    return a1 + a2 + a3;
    }
    };

    struct S
    {
    std::string s;
    int f()
    {
    }
    };

    int main(int /*argc*/, char* /*argv*/[])
    {
    X x;

    using namespace mbind;

    std::cout << bind(&f4, 1, 2, 3, 4)() << "\n"
    << bind(&f4, 1, _1, _2, _3)(2, 3, 4) << "\n"
    << bind(&X::f, &x, _1, _2, 3)(2, 3) << "\n"
    << bind(&f1, _1)(2) << "\n"
    << bind(&X::f1, &x, _1)(2) << "\n"
    << bind(&f3, 1, _1, _2)(2, 3) << "\n"
    << bind(&X::f0, &x)() << "\n"
    << bind(&f0)() << "\n"
    << bind(std::plus<int>(), bind(&f1, _1), _1)(2) << "\n"
    << bind(std::plus<int>(), bind(&f2, _1, 2), _1)(1) << "\n"
    << bind(std::plus<int>(), bind(&f1, _1), _2)(1, 2) << "\n"
    << bind(std::plus<int>(), bind(&f1, _1), _2)(1, 2) << std::endl;

    S s;
    s.s = "data";

    std::cout << bind(&S::s, _1)(s) << "\n"
    << bind(&S::s, &s)() << std::endl;
    }


    Основная идея проста:

  • Есть некоторый объект, который формируется, принимая некий параметр для действия и набор параметров этого самого действия, включая "заместители" в стиле boost::bind
  • Этот объект содержит принимаемые в конструкторе параметры по значению в качестве своих членов данных
  • Имеется переопределенный оператор (), который позволяет инициировать выполнение действия с нужными пользователю параметрами

    Главный вопрос, который возникает сразу же - как обрабатывать "заместители" (в дальнейшем, для удобства, будем называть их placeholder'ами, чтобы не отступать от имеющейся терминологии bind).

    Лобовое решение выглядит просто, но изящно: мы будем трактовать все члены данных, кроме самого первого, как функторы, которые принимают в качестве параметров весь набор данных, переданных пользователем.

    Для определенности, рассмотрим сейчас случай bind'a с четырьмя аргументами - два аргумента, которые обычно в таких случаях рассматривют, вроде смешно, число три я как-то исторически не люблю, так что четыре будет в самый раз :-)

    Итак, определяем способ, которым можно указать, какой из параметров является placeholder'ом:
    template<int N> struct arg
    {
    };

    static arg<1> _1;
    static arg<1> _2;
    static arg<1> _3;
    static arg<1> _4;
    Отлично, теперь у нас появилась возможность задавать placeholder'ы со стороны пользователя.

    Определим "наивную" реализацию объекта, который мог бы хранить действие, начальные параметры, а также информацию по привязке параметров при последующих вызовах ():
    template<typename R, typename F, typename A1, typename A2, typename A3, typename A4>
    struct bind_obj
    {
    bind_obj(A1 a1, A2 a2, A3 a3, A4 a4)
    {
    // ....
    }

    R operator(A1 a1, A2 a2, A3 a3, A4 a4)
    {
    // ....
    }
    };
    Для начала отмечу, что мы только что написали очевидно антиоптимальный код, поскольку все параметры
    передаются по значению. Конечно, можно их сделать ссылками, но это не будет работать - позже мы к этому вернемся...

    Вторым моментом следует упомянуть, что пользователь во время создания объекта может употребить разное количество placeholder'ов, от нуля до четырех включительно:
    template<typename R, typename F, typename A1, typename A2, typename A3, typename A4>
    struct bind_obj
    {
    bind_obj(A1 a1, A2 a2, A3 a3, A4 a4)
    {
    // ....
    }

    R operator()
    {
    // ....
    }

    R operator(A1 a1)
    {
    // ....
    }

    R operator(A1 a1, A2 a2)
    {
    // ....
    }

    R operator(A1 a1, A2 a2, A3 a3)
    {
    // ....
    }

    R operator(A1 a1, A2 a2, A3 a3, A4 a4)
    {
    // ....
    }
    };
    Определимся с placeholder'ами:
    template<typename A>
    struct placeholder
    {
    A a_;

    placeholder(A a): a_()
    {
    }

    template<typename A1, typename A2, typename A3, typename A4>
    A operator(A1 a1, A2 a2, A3 a3, A4 a4)
    {
    return a_;
    }
    };
    Что у нас получилось? Класс, который принимает один параметр, а затем возвращает его значение вне зависимости от того, что подано на вход метода ().
    Это прекрасно обеспечивает начальную привязку параметров в нашем будущем bind.

    Рассмотрим реализацию самого метода ():
    struct empty()
    {
    };

    template<typename R, typename F, typename A1, typename A2, typename A3, typename A4>
    struct bind_obj
    {
    F f_;
    placeholder h1_;
    placeholder h2_;
    placeholder h3_;
    placeholder h4_;

    bind_obj(A1 a1, A2 a2, A3 a3, A4 a4): h1_(a1), h2_(a2), h3_(a3), h4_(a4)
    {
    }

    R operator()
    {
    return f_(
    h1(empty(), empty(), empty(), empty() ),
    h2(empty(), empty(), empty(), empty() ),
    h3(empty(), empty(), empty(), empty() ),
    h4(empty(), empty(), empty(), empty() )
    );
    }

    template<typename CA1>
    R operator(A1 a1)
    {
    return f_(
    h1(a1, empty(), empty(), empty() ),
    h2(a1, empty(), empty(), empty() ),
    h3(a1, empty(), empty(), empty() ),
    h4(a1, empty(), empty(), empty() )
    );
    }

    // ...
    template<typename CA1, typename CA2, typename CA3, typename CA4>
    R operator(CA1 a1, CA2 a2, CA3 a3, CA4 a4)
    {
    return f_(
    h1(a1, a2, a3, a4),
    h2(a1, a2, a3, a4),
    h3(a1, a2, a3, a4),
    h4(a1, a2, a3, a4)
    );
    }
    };
    Теперь определим некоторые специализации placeholder'а, которые, собственно, и позволят осуществить привязку параметров на момент конструирования объекта:
    template<>
    struct placeholder<arg<1> >
    {
    A a_;

    placeholder(A a): a_()
    {
    }

    template<typename A1, typename A2, typename A3, typename A4>
    A operator(A1 a1, A2 a2, A3 a3, A4 a4)
    {
    return a1;
    }
    };

    template<>
    struct placeholder<arg<2> >
    {
    A a_;

    placeholder(A a): a_()
    {
    }

    template<typename A1, typename A2, typename A3, typename A4>
    A operator(A1 a1, A2 a2, A3 a3, A4 a4)
    {
    return a2;
    }
    };

    //...
    Замечательно! Берем такую функцию:
    int f4(int a1, int a2, int a3, int a4)
    {
    return a1 + a2 + a3 + a4;
    }
    и конструируем нашу "наивную" заготовку:
    std::cout << bind_obj<int, int (*)(&f4, int, int, int, int), 1, _1, _2, 4)(2, 3) << std::endl;
    Что у нас получится? Первый и четвертый placeholder'ы запомнят начальные параметры вызова, которые и вернутся при вызове оператора (), а для второго и третьего сработают созданные специализации, которые, соответственно, вернут второй и третий аргументы вызова(), после чего все отправится в функтор f_.
    Просто замечательно!

    Теперь формируем функцию bind, которая возвращает наш объект:
    template<typename R, typename F, typename A1, typename A2, typename A3, typename A4>
    bind_obj<R, F, A1, A2, A3, A4> bind(F f, A1 a1, A2 a2, A3 a3, A4 a4)
    {
    return bind_obj<R, F, A1, A2, A3, A4>(a1, a2, a3, a4);
    }
    Получается почти замечательно... вот только не совсем.

    Что плохо? Таких моментов тут несколько:
  • Приходится каждый раз городить очень неудобную запись, которая далека от совершенства, поскольку автовыведение параметров шаблонной функции не сработает
  • Приходится явно указывать тип возвращаемого значения, причем дважды(!)
  • Если предположить, что параметры функции не скаляры, то возникают проблемы с производительностью, вызванные лишним копированием объектов
  • При вызове нашего объекта нет четкой привязки количества placeholder'ов к количеству реальных аргументов (), что будет порождать трудноуловимые ошибки, спасибо нам пользователи за это не скажут.

    Таким образом, встают три основные задачи:
  • Обеспечить работу автовыведения типов для шаблонной функции
  • Решить вопрос привязки количества placeholder'ов к количеству реальных аргументов
  • Решить вопросы производительности

    Займемся первым - что нам делать с мешающим параметровм R?
    Тут нам на помощь приходит метапрограммирование в стиле Александреску &: Co :-)
    template<typename T> struct result_of;

    template<typename R> struct result_of<R (*)()>
    {
    typedef R type;
    };

    template<typename R, typename A1> struct result_of<R (*)(A1)>
    {
    typedef R type;
    };

    template<typename R, typename A1, typename A2> struct result_of<R (*)(A1, A2)>
    {
    typedef R type;
    };

    template<typename R, typename A1, typename A2, typename A3> struct result_of<R (*)(A1, A2, A3)>
    {
    typedef R type;
    };

    template<typename R, typename A1, typename A2, typename A3, typename A4> struct result_of<R (*)(A1, A2, A3, A4)>
    {
    typedef R type;
    };

    template<typename R, typename C> struct result_of<R (C::*)>
    {
    typedef R type;
    };


    Что мы сделали? Объявили шаблон result of, который распознает несколько необходимых нам ситуаций с указателями на функции от разного количества аргументов и функторами.

    Теперь наш код можно очень сильно упростить:
    template<typename R, typename F, typename A1, typename A2, typename A3, typename A4>
    struct bind_obj
    {
    result_of<F> operator()
    {
    // ...
    }

    template<typename CA1>
    result_of<F> operator()(CA1 a1)
    {
    // ...
    }
    };

    template<typename F, typename A1, typename A2, typename A3, typename A4>
    bind_obj<F, A1, A2, A3, A4> bind(F f, A1 a1, A2 a2, A3 a3, A4 a4)
    {
    return bind_obj<F, A1, A2, A3, A4>(a1, a2, a3, a4);
    }
    Использование этого кода уже близко к тому, что нам хотелось бы достичь:
    std::cout << bind(&f4, 1, _1, _2, 4)(2, 3) << std::endl;


    Для решения второй проблемы лобовой способ выглядит просто - нас нужно превратить bind_obj в шаблонный класс, одним из параметров которого является количество placeholder'ов. Тогда оператор () в структуре будет только один, ровно соответствующий по количеству параметров числу placeholder'ов при вызове конструктора.
    template<typename T>
    struct is_placeholder
    {
    enum { arg_count = 0 };
    };

    template<>
    struct is_placeholder<arg<1> >
    {
    enum { arg_count = 1 };
    };

    template<>
    struct is_placeholder<arg<2> >
    {
    enum { arg_count = 2 };
    };

    template<>
    struct is_placeholder<arg<3> >
    {
    enum { arg_count = 3 };
    };

    template<>
    struct is_placeholder<arg<4> >
    {
    enum { arg_count = 4 };
    };

    template<int N1, int N2>
    struct select_max
    {
    enum { result = N1 > N2? N1 : N2 };
    };

    template <typename A1, typename A2, typename A3, typename A4>
    struct placeholder_arg_counter_4
    {
    enum
    {
    value = select_max<
    is_placeholder<A4>::arg_count,
    select_max<
    is_placeholder<A3>::arg_count,
    select_max<
    is_placeholder<A2>::arg_count,
    select_max<is_placeholder<A1>::arg_count,0>::result
    >::result
    >::result
    >::result
    };
    };
    Мы объявляем шаблон is_placeholder, который содержит константу arg_count, показывающую количество placholder'ов в предположении, что пользователь не будет объявлять такие вот конструкции:
    bind(&f4, 1, 2, _1, _4);
    Для каждого известного _{i} [1-4] мы объявляем свою специализацию.
    Далее определяем рекурсивный поиск максимального числа на заданном наборе результатов is_placeholder::arg_count.

    Имея такой инструментарий можно объявить:
    template<typename F, typename A1, typename A2, typename A3, typename A4>
    bind_obj_4<F, A1, A2, A3, A4, placeholder_arg_counter_4<A1, A2, A3, A4>::value >
    bind(F f, A1 a1, A2 a2, A3 a3, A4 a4)
    {
    return bind_obj<F, A1, A2, A3, A4, placeholder_arg_counter_4<A1, A2, A3, A4>::value >(f, a1, a2, a3, a4);
    }
    и специализацию bind_obj для каждого случая [0-4], снабжая ее единственным оператором ().

    Забегая вперед (потом мы к этому вернемся), сейчас будет очень полезно переименовать bind_obj в bind_obj_4, подчеркивая лишний раз - с каким количеством параметров потенциально может обращаться объект. Это же относится и к placeholder.

    Решение вопроса производительности лежит в определении момента, когда нужно использовать в параметрах шаблона ссылку на тип, а когда и сам тип. Наивная идея просто поставить руками ссылки везде не работает, поскольку ссылок на ссылки в C++ не бывает - шаблон становится недостаточно общим).

    Например:
    typedef S& RS;
    typedef RS& RRS;
    Компиляция такого кода приводит к ошибке (для MSVC error C2529: 'RRS' : reference to reference is illegal).

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

    Продолжение следует...
  • Комментариев нет: