Буст::лямбда своими руками
От: Vamp Россия  
Дата: 29.07.09 18:01
Оценка: 15 (4)
Сразу предупрежу, что настоящей буст::лямбды в результате не получится. Название есть не более чем рекламный трюк. С другой стороны, особого смысла сочинять настоящую буст::лямбду я не вижу. Лучше, чем уже существуюшая не получится, хуже не хотелось бы, а желающих сочинить буст::лямбду, идентичную уже существующей, я отсылаю к великолепному рассказу Борхеса "Пьер Менар, автор дона Кихота".
Скорее, мне было интересно разобраться, какие возможности предлагает современная реализация С++ для желаюших создать библиотеку поддердки лябмда-выражений. То что у меня получилось, конечно, мало подходит для использования в реальных приложениях, однако, на мой взгляд, представляет определенный интерес с образовательной точки зрения. Новичкам так же могут оказаться полезны парочка примененных здесь трюков.

Итак, постановка задачи. Проще всего показать, что мне надо от разрабатываемой библиотеки:


int main() {
    vector<float> v(3);

    v[0] = 1;
    v[1] = 2;
    v[2] = 3;

    int i = 0;
    for_each(v.begin(), v.end(), _1 =  _1 + 6 );  // (*)

    for_each(v.begin(), v.end(), cout << _1 + 8.3 << ": " <<  _1 << "\n"); // (**)
    for_each(v.begin(), v.end(), var(i) = _1 + var(i)); // (***)

}


Хочется, чтобы в результате работы этой программы были выведены следующие строки:


15.3: 7
16.3: 8
17.3: 9
24



В строке (*) мы пытаемся удобным образом изменить значение элементов последовательности. Я попытался написать аналог того же в "классическом" стиле, с использованием bind1st и plus<float>, но не преуспел. В любом случае, на мой взгляд, эта запись существенно нагляднее, чем любая мешанина из bind.
Строчка (**) выводит значение каждого элемента, а так же сумму его значения и 8.3. Смысла в сумме особого нет, просто демонстрация возможностей. Опять же, как мне кажется, такая запись нагляднее, чем связка copy и ostream_iterator.
И наконец, весьма уродливая третья строчка (***) суммирует элементы последовательности. Да, выглядит мерзко, хотелось бы использовать что-то вроде i = _1 + i, но увы, вероятно это невозможно. Дело в том, что переопределить оператор = для встроенного типа нельзя. Вариант var(i) = i + _1 вполне осуществим, просто мне было лень с этим возиться.

Как же все это работает?
Основными конструктивными элементами данной библиотеки являются плейсхолдер, функтор и вариатор. В классическом случае плейсхолдер — это объект не содержащий данных, и содержащий ровно две функции — оператор присваивания и (). Оператор () просто возвращает свой аргумент, а оператор присваивания подготавливает плейсхолдер к последующему применению оператора ().
Благодаря наличию оператора () становятся возможны конструкции вида

(_1)(10); // результат - 10


а оператор присваивания позволяет сочинять что-то вроде

(_1 = 6)(i); // переменная i равна 6


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

(_1 + 4)(10)


вернет 14.
Вариатор — это специальный объект, который сохраняет переданную ему в конструкторе ссылку значение внутри себя и в дальнейшем позволяет собой манипулировать. Вариатор нужен, чтобы работал оператор присваивания, который нельзя переопределить для встроенных типов, а так же для хранения ссылок на объекты, копировать которые невозможно — к таким, например, относится cout.

Теперь приведу определения всех этих типов. Начну с плейсхолдера, ибо он незатейлив, как грабли.

struct placeholder {
    template <class R> equator<R>  operator= (const R& r) {return equator<R>(r);}
};


Вот и весь плейсхолдер. Уравниватель (equator) — это специальный объект, для которого определен оператор (). Я приведу его определение чуть позже. Нужен он потому, что собственно присваивание выполняется после, когда передан присваиваемый аргумент, а не в тот момент, когда компилятор видит оператор =. Возможно, это станет неможечко более понятно, если привести такой пример:


int i = 0, j=10, k=20;
E e(_1 = 10);
e(i);
e(j);
e(k);


Здесь E = это условный тип экватора. В результате выполнения этого кода i, j и k станут равны 10.

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


template<class T> struct var_holder {
    T& var;
    var_holder(T& t) : var(t) {}
    template <class R> equator_1<T&, R>  operator= (const R& r) {return equator_1<T&, R>(var, r);}
};

template <class T> var_holder<T> var(T& var) {return var_holder<T>(var);}


Функция var — вспомогательная функция, которая позволяет создать объект типа var_holder без явного указания типа хранимого значения. Для вариатора нужна собственная версия уравнивателя, уравниватель плейсхолдера не подходит. Причина в том, что уравниватель для вариатора должен хранить оба значения — слева и справа от знака =, и выполнять присваивание в операторе (). Уравниватель для плейсхолдера хранит только правое значение, так как слева находится плейсхолдер, не содержащий никого значения сам по себе.

Пришло время познакомиться с функтором.
Функтор — это объект, выполняющий отложенные вычисления. (_1 + 5) — это функтор, результат которого станет известен только тогда, когда значение будет передано ему в операторе ().
(_1 + 5 + 4) — это два функтора. Аргументами первого являются _1 и 5, второго — первый функтор и 4. (_1 + 5 + 4)(10) выполняется рекурсивно. Сначала считается внутренний функтор, потом внешний.
Вот определение функтора:


template <class L, class R, class F> struct functor {
       L lhs;
       R rhs;
       F f;
       functor(L l_, R r_, F f_) : lhs(l_), rhs(r_), f(f_) {}
       template <class T> typename ref_builder<L, T>::type operator() (T& a) {return f(ref_builder<L, T>::ref(lhs, a), ref_builder<R, T>::ref(rhs, a));}
};


Выглядит несложно. На самом деле, основная соль тут в ref_builder, который было бы правильнее назвать deducer. Это вспомогательный класс, назначение которого состоит в определении возвращаемого значения для оператора (), а также вызов этого оператора для двух аргументов функтора. ref_builder использует частичную специализацию, чтобы выбирать тип возвращаемого значения и реализацию оператора () в зависимости от своих аргументов.

Приведу все его версии с комментариями.

Это самая общая версия ref_builder. Она игнорирует второй аргумент и всегда возвращает хранимое значение. При вычислении выражения (_1 + 4 + 3)(10) она будет вызвана с аргументами 3 и 10 и вернет 3. Она очевидным образом определяет тип возвращаемого значения как тип хранимого значения.


template <class TP, class T> struct ref_builder {
      typedef const TP& type;
      static type ref(type t, T) {return t;}
};



Это специализация ref_builder для плейсхолдера. Всегда возвращает второй аргумент, потому что у плейсхолдера возвращать нечего. Тип возвращаемого значения совпадает с типом второго аргумента. При вычислении выражения (_1 + 4 + 3)(10) она будет вызвана с аргументами _1 и 10 и вернет 10.


template <class L> struct ref_builder<placeholder, L> {
      typedef L type;
      static type ref(placeholder p, type a) {return a;}
};


Дальше идет специализация ref_builder для вариатора. Ее единственное отличие от общей версии состоит в том, что возвращается значение, сохраненное в вариаторе. При вычислении выражения (_1 + ref(i))(10) вернет i.


template <class L, class R> struct ref_builder<var_holder<L>, R> {
      typedef L& type;
      static type ref(var_holder<L> p, R&) {return p.var;}
};


Ну и наконец, самая последняя специализация — для функтора. Тип возвращаемого значения рекурсивно считается вглубь до конца, а в при вычислении выражения (_1 + 4 + 3)(10) она вызовется с параметрами (_1 +4), 10. На первый взгляд может показаться, что подобное вычисление возвращаемого типа "вглубь" не нужно, и вполне можно ограничиться типом передаваемого значения, но это не так. Если для функтора вида (_1 + 4) тип возвращаемого значения действительно совпадает с аргументом, то у функтора (cout << _1) тип возвращаемого значения есть тип объекта cout.


template <class L, class R, class F, class T> struct ref_builder<functor<L, R, F>, T > {
      typedef typename ref_builder<L, T>::type type;
      template <class A> static type ref(functor<L, R, F> p, A& a) {return p(a);}
};


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

Уравниватель для плейсхолдера. Тип возвращаемого значения определяется с помощью ref_builder, присваивание выполняется для аргумента.


template <class R> struct equator {
       R r;
       equator(R r_) : r(r_) {}
       template <class T> const T& operator() (T& b) { return b = ref_builder<R, T>::ref(r, b); }
};


Уравниватель для вариатора. Тип возвращаемого значения определяется с помощью ref_builder, присваивание выполняется для хранимого значения с использованием аргумента.


template <class L, class R> struct equator_1 {
       R r;
       L l;
       equator_1(L l_, R r_) : r(r_), l(l_) {}
       template <class T> const L operator() (T& b) { return l = ref_builder<R, T>::ref(r, b); }
};



Итак, с основными типами мы разобрались. Теперь надо добавить собственно определения операторов, чтобы все это заработало. В принципе, все они тривиальны и сводятся к созданию нужных объектов. Запись, правда, выходит довольно громоздкая.
Определим бинарный плюс. Нам потребуются две его версии, одна принимающая плейсхолдер и что угодно, вторая — функтор и что угодно. При желании можно определить версию, принимающую вариатор и что угодно, тогда будет работать ref(i) + _1 — в текущей версии плейсхолдер или функтор должен стоять слева. Кроме типа левого аргумента, никакого отличия в этих функциях нет, и казалось бы, можно было бы заменить их на одну шаблонную, принимающую "что угодно" и "что угодно". Но это было бы не очень хорошим решением, потому что позволяло бы суммировать несуммироемое, например:


class Apples {...};
class Oranges {...};
Apples operator + (Apples a, Apples b);
Oranges operator + (Apples a, Apples b);
...
Apples a, a1, a2;
Oranges o;

a = a1 + a2 + o;


При наличии "общего" плюса такое суммирование было бы корректно с точки зрения компилятора, и ошибка бы возникала при попытке присвоения объекту типа Apples объекта типа functor<...>. Принимае во внимание "шаблонность" функтора, сообщение об ошибке заняло бы примерно пол экрана, и разобраться в нем было бы непросто. Уж лучше увидеть нормальное сообщение о том, что яблоки с апельсинами складывать нельзя.


template <class R> functor<placeholder, R, placeholder_plus> operator+ (placeholder lhs, const R& rhs) {
       return functor<placeholder, R, placeholder_plus>(lhs,  rhs, placeholder_plus()) ;
}

template <class R, class A, class B, class C> functor<functor<A, B, C> , R, placeholder_plus> operator+ (functor<A, B, C> lhs, const R& rhs) {
       return functor<functor<A, B, C>, R, placeholder_plus>(lhs,  rhs, placeholder_plus()) ;
}


Сам класс placeholder_plus на редкость примитивен:


struct placeholder_plus {
    template <class L, class R> L operator() (const L& a, const R& b) {return a + b;}
};


Тут есть только одна тонкость. Так как тип возвращаемого значения оператора () совпадает с типом левого операнда, для выражений типа (_1 + 5.5)(10) вернется 15, а не 15.5, так как тип плейсхолдера в конечном итоге целый. Если же задать тип левого и правого аргумента одним и тем же, то такая конструкция компилироваться вообще не будет, а кроме того, не позволит складывать яблоки и апельсины, даже если для них определен оператор сложения.

Точно таким же образом определяются все прочие бинарные арифметические операции (-, /, *), и останавливаться на них я не хочу.
На чем я хочу и остановлюсь, это на реализации сдвига влево, который нам интересен на сам по себе, а как оператор, перегруженный для потокового вывода.
Здесь есть несколько тонкостей. Первая и основная состоит в том, что для объектов ostream копирование запрещено. Описанные выше функторы постоянно копируют лежашие внутри объекты, так что cout (или любой другой объект, для которого реализован оператор <<) нам придется поместить внутрь вариатора. А вариатор можно копировать, пока не надоест.

По тем же причинам, что и для оператора +, нам придется реализовать несколько версий оператора <<.

Вот они.

Версия для плейсхолдера:


template <class L> functor<var_holder<L>, placeholder,  placeholder_shift> operator<< (L& stream_obj, placeholder rhs) {
       return functor<var_holder<L>, placeholder, placeholder_shift >(var_holder<L>(stream_obj),  rhs, placeholder_shift()) ;
}



Версия для вариатора. Требуется в конструкциях такого вида for_each( ... cout << var(":"} << _1);

Здесь если не превратить ":" в вариатор, а написать просто cout << ":" << _1, то двоеточие выведется на экран только один раз — перед самым первым элементом.


template <class L, class R> functor<var_holder<L>, var_holder<R>,  placeholder_shift> operator<< (L& stream_obj, var_holder<R> rhs) {
       return functor<var_holder<L>, var_holder<R>, placeholder_shift >(var(stream_obj),  rhs, placeholder_shift()) ;
}


Версия с функтором. Используется в таких конструкциях: cout << (_1 + 6).


template <class L, class A, class B, class C> functor<var_holder<L>, functor<A, B, C>,  placeholder_shift>
   operator<< (L& stream_obj, const functor<A, B, C>& f) {
       return functor<var_holder<L>, functor<A, B, C>, placeholder_shift >(var_holder<L>(stream_obj), f, placeholder_shift()) ;
}


И наконец, реализация оператора сдвига для функтора — полезен, если писать примерно так: cout << _1 << "\n":


template <class R, class A, class B, class C>
  functor<functor<A, B, C>, R,  placeholder_shift> operator<< (functor<A, B, C> lhs, R rhs)
{
       typedef functor<A, B, C> type;
       return functor<type, R, placeholder_shift >(lhs,  rhs, placeholder_shift()) ;
}


Вот, собственно, и все.
Еще раз повторюсь, я не претендовал на полноценную реализацию лямбда-выражений. К очевидным недостаткам приведенной здесь программы относятся, во-первых, большое количество копирований в аргументах функций, что не только влияет на производительность, но и накладывает ограничение на используемые объекты, для которых должен быть доступен конструктор копирования. Кроме того, определения типов как ссылок на парметры шаблона (typdef L& type) чревато тем, что L уже будет ссыкой и подобное выражение компилироваться не будет. Так что подчеркну, этот код имеет скорее учебную, чем практическую ценность.

Весь код целиком (один файл, бери и компилируй) можно скачать тут: http://files.rsdn.ru/6614/blamba.C
Да здравствует мыло душистое и веревка пушистая.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.