воскресенье, 30 мая 2010 г.

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

Продолжение истории про boost::bind, начатой тут

Теперь можно задуматься над расширением нашей библиотеки - количество параметров в вызываемой функции (функторе/методе класса) не обязательно равно четырем.

Чтобы решить эту проблему я предлагаю воспользоваться кодогенератором.

Причин поступить именно так, помимо моей личной лени, достаточно много:
  • Мы хотим иметь 9 параметров - столько, сколько в boost::bind(а то и больше), а это много
  • Мы не хотим прибегать к слишком уж продвинутому программированию
  • Мы хотим, чтобы система, которая использует нашу реализацию, компилировалась бы быстро, в отличие от многих предлагаемых вариантов

    Эти требования сразу же решают судьбу списков типов в стиле Александреску и и BOOST_PREPROCESSOR - мы хотим сделать сразу работающий вариант, который не требуется каждый раз долго и мучительно компилировать, для больших проектов такие "гениальные" модули могут замедлять сборку очень серьезно...

    В качестве инструмента возьмем Perl - это все еще неплохое средство для работы с текстами, несмотря на тотальное засилие Python :-)

    Немного перетасуем наши исходные коды - все разобъем условно на три части:
  • Пролог, содержащий коды, независимые от количества аргументов bind_obj
  • Собственно реализацию bind_obj и вспомогательных средств, например result_of для заданного количества аргументов
  • Эпилог, декларирующий _1, _2, _3 etc. Их, кстати, нужно поместить вне namespace mbind, для того, чтобы их можно было найти.

    Каждой из частей мы сопоставим одну подпрограмму Perl (потом вторую подпрограмму мы вызовем потом столько раз, сколько нужно иметь аргументов).

    Следующий шаг - выделение паттернов, которые будут общими для каждой реализации bind_obj_{n}:
  • typename A1, typename A2, typename A3, typename A4
  • A1 a1, A2 a2, A3 a3, A4 a4
  • A1, A2, A3, A4
  • a1, a2, a3, a4
  • CA1, CA2, CA3, CA4
  • CA1 a1, CA2 a2, CA3 a3, CA4 a4
  • ....

    Для каждого найденного паттерна организуем собственную переменную, используя конструкцию:
    my $ var = map { "xxx$_" } (1..$arg_count)
    Здесь используется как интерполяция переменных Perl, так и неименованная подпрограмма.
    Для остального вывода удобнее всего употребить ряд конструкций "document here" вида
    print<<EOT;
    my string1 $arg_count
    my string2
    EOT
    Здесь также будет использована интерполяция переменных, нужно только помнить, что при возникновении неоднозначных трактовок имя переменной следует заключать в пару скобок "{}".

    Все деликатные внутренние моменты реализуем с помощью обычного оператора for () {}.

    Да, use strict использовать обязательно, иначе отлаживаться можно до второго пришествия, а мы используем генератор чтобы экономить время...

    Собственно программа укладывается в несколько строчек:
    print<<EOT;
    my $ARGS=9;
    header $ARGS;
    for (1..$ARGS) {
    body $_;
    }
    footer $ARGS;
    Добиться работоспособности генерируемого кода можно очень быстро.

    Обращаю внимание, body на самом деле должна вызываться, начиная с нулевого количества параметров, после чего все сразу перестает компилироваться, какое-то время уходит на адаптацию кода к отсутствию аргументов (это резко усиливает бардачность), но оно того стоит...

    Эти 400 строк высоконечитабельного кода, которые дают нам нужный результат (ценителей Perl прошу смотреть в другую сторону) можно взять здесь, а полученный заголовок здесь. Полный проект MSVC2003, полный проект можно взять здесь.

    Тестовый набор, который отработал (даже несколько более широкий, чем изначально изначально планировалось):
    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, _1)(4) << "\n"
    << 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, s)() << "\n"
    << bind(&S::s, _1)(s) << "\n"
    << bind(&S::s, &s)() << std::endl;

    std::string s1("1"), s2("2");

    std::cout << bind(std::plus<std::string>(), _1, _2)(s1, s2) << std::endl;
    }
    Положа руку на сердце, это не тесты, а бессистемное УГ, если бы мне на рабочем месте их предложили, я бы сказал много интересного, но тут уже силы как-то на исходе...

    Что можно сказать относительно полученного результата?
  • Относительно легко понимаемая идея
  • Возможность легко задавать более 9 аргументов... вот только я бы лично за такие функции/методы руки бы отрывал.
    Единственный случай, когда их можно терпеть в коде - использование какого-нибудь олдскульного API ;-)

    А так, почти ничего особо хорошего:
  • Да, проблему мы "в лоб" почти решили, изящество подхода под большим сомнением
  • Корректность работы должна подтверждаться систематически составленным тестовым набором, его у нас пока нет (относительно корректной работы вложенных связывателей, например, у меня есть серьезные сомнения)
  • Вопрос производительности так и остался открытым (проиллюстрировано ниже на примере bind(&S::s, _1)(s) )
    00404854  lea         edx,[esp+110h] 
    0040485B mov dword ptr [ecx+18h],esi
    0040485E mov dword ptr [ecx+14h],ebx
    00404861 push edx
    00404862 mov byte ptr [esp+13Ch],1
    0040486A mov byte ptr [ecx+4],bl
    0040486D call std::basic_string<char,std::char_traits<char>,std::allocator>char> >::assign (402010h)
    00404872 lea eax,[esp+78h]
    00404876 push eax
    00404877 lea ecx,[esp+3Ch]
    0040487B mov dword ptr [esp+3Ch],ebx
    0040487F call mbind::bind_obj_1<std::basic_string<char,std::char_traits<char>,std::allocator<char> > S::*,mbind::arg<1>,1>::operator()<S>
    (404210h)


    Код с оригинальным boost выглядит несколько иначе:
    004034EC  push        ebx  
    004034ED mov dword ptr [esi+18h],0Fh
    004034F4 mov dword ptr [esi+14h],ebx
    004034F7 push eax
    004034F8 mov ecx,esi
    004034FA mov byte ptr [esi+4],bl
    004034FD call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign (402010h)
    00403502 cmp dword ptr [esp+24h],10h
    00403507 jb std::operator+<char,std::char_traits<char>,std::allocator<char> >+86h (403516h)
    00403509 mov edx,dword ptr [esp+10h]
    0040350D push edx
    0040350E call operator delete (406B5Dh)
    00403513 add esp,4
  • Вопрос с переносимостью кода решается просто - достаточно задействовать тот же codepad - выяснится, что поддержке нескольких компиляторов придется уделить самое серьезное внимание.

    Как только мы устраним все вопросы, объем кода устремится к оригиналу, а его понятность упадет ниже плинтуса...

    Вывод прост - как игра ума это все забавно, в Production коде же желательно использовать проверенный boost::bind, или его аналог из lambda :-)

    BTW, функторы в Loki тоже ведут себя не самым лучшим образом, что народ подтверждает :-)
  • Комментариев нет: