Итак, что у нас в пререквизитах:
Типовые примеры, которые должны обслуживаться:
#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;
}
Основная идея проста:
Главный вопрос, который возникает сразу же - как обрабатывать "заместители" (в дальнейшем, для удобства, будем называть их 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);
}
Получается почти замечательно... вот только не совсем.Что плохо? Таких моментов тут несколько:
Таким образом, встают три основные задачи:
Займемся первым - что нам делать с мешающим параметровм 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).Откровенно говоря, решать эту задачу сейчас не очень хочется - а код и так получится достаточно непростым для понимания :-)
Возможно, к вопросу оптимизации мы вернемся позднее.
Продолжение следует...
Комментариев нет:
Отправить комментарий