Почему изменяемые лямбда-выражения, захватывающие инициализацию, не могут иметь изменяемые элементы данных?

Этот вопрос связан с предыдущим, где было замечено, что mutable лямбды init-capture несовместимы с диапазоном Boost и итератором transform для некоторых довольно неясных и глубоко вложенных сбоев typedef, которые могут или не могут быть легко устранены путем взлома исходников Boost.Range.

В принятом ответе предлагалось хранить лямбда в объекте std::function. Чтобы избежать потенциальных накладных расходов на вызов функции virtual, я написал два объекта функций, которые могли бы служить потенциальными обходными путями. В приведенном ниже коде они называются MutableLambda1 и MutableLambda2.

#include <iostream>
#include <iterator>
#include <vector>
#include <boost/range/adaptors.hpp>
#include <boost/range/algorithm.hpp>

// this version is conforming to the Standard
// but is not compatible with boost::transformed
struct MutableLambda1
{
    int delta;     
    template<class T> auto operator()(T elem) { return elem * delta++; }
};

// Instead, this version works with boost::transformed
// but is not conforming to the Standard
struct MutableLambda2
{
    mutable int delta;
    template<class T> auto operator()(T elem) const { return elem * delta++; }
};

// simple example of an algorithm that takes a range and laziy transformes that
// using a function object that stores and modifies internal state
template<class R, class F>
auto scale(R r, F f) 
{
    return r | boost::adaptors::transformed(f);
}

int main()
{
    // real capturing mutable lambda, will not work with boost::transformed
    auto lam = [delta = 1](auto elem) mutable { return elem * delta++; };        
    auto rng = std::vector<int>{ 1, 2, 3, 4 };

    //boost::copy(scale(rng, lam), std::ostream_iterator<int>(std::cout, ","));                 /* ERROR */
    //boost::copy(scale(rng, MutableLambda1{1}), std::ostream_iterator<int>(std::cout, ","));   /* ERROR */
    boost::copy(scale(rng, MutableLambda2{1}), std::ostream_iterator<int>(std::cout, ","));     /* OK!   */
}

Живой пример, который не компилирует строки с lam и MutableLambda1, и правильно печатает 1, 4, 9, 16 для строки с MutableLambda2.

Однако проект стандарта упоминает

5.1.2 Лямбда-выражения [expr.prim.lambda]

5 [...] Этот оператор вызова функции или шаблон оператора объявляется const (9.3.1) тогда и только тогда, когда за пунктом объявления параметра лямбда-выражения не следует mutable. [...]

11 Для каждого init-capture нестатический элемент данных, названный идентификатором init-capture, объявляется в типе замыкания. Этот элемент не является битовым полем и не mutable. [...]

Это означает, что MutableLambda2 не является подходящей рукописной заменой для лямбда-выражения mutable, захватывающего инициализацию.

Вопросы

  • почему реализация init-capture mutable lambdas такая, какая она есть (т.е. неконстантный оператор вызова функции)?
  • почему кажущаяся эквивалентной альтернатива mutable элементов данных с const оператором вызова функции запрещена?
  • (бонус), почему диапазон Boost и итератор transform основаны на том факте, что объект функции operator() равен const?

person TemplateRex    schedule 22.02.2014    source источник
comment
Извините, я туплю, но не могли бы вы привести очень короткий, бессильный пример того, что вы хотели бы уметь делать, но не можете?   -  person Kerrek SB    schedule 23.02.2014
comment
@KerrekSB Боюсь, этот вопрос действительно связан с Boost.Range. Я хочу лениво преобразовать диапазон чисел, используя алгоритм, который требует сохранения и изменения некоторого внутреннего состояния в объекте функции. Изменяемые лямбда-выражения с захватом инициализации кажутся подходящим способом, но дают пресловутый шаблонный роман. MutableLambda2 работает, но не имеет такой же реализации, как Стандарт предписывает для лямбда-выражений. Интересно, почему это так, и есть ли у моего обходного пути какие-либо скрытые ловушки.   -  person TemplateRex    schedule 23.02.2014
comment
Вы говорите, что MutableLambda2 не соответствует Стандарту. В каком смысле? Как реализация лямбда-выражений? -- Изменяемые лямбда-выражения с неконстантными операторами вызова функций требуют вызова неконстантных объектов; Константные операторы вызова функций OTOH + mutable члены данных этого не делают. Это та разница, которую вы ищете?   -  person dyp    schedule 23.02.2014
comment
@dyp да, предположим, что компилятор выдаст MutableLambda2 вместо MutableLambda1 (как сейчас требуется), как он сломает пользовательский код? Как вы указываете, вы могли бы вызывать первое для объектов const, но почему это плохо? (поскольку он изменяет только внутреннее состояние, а не аргументы, передаваемые в operator()).   -  person TemplateRex    schedule 23.02.2014
comment
@TemplateRex Может быть, вы можете ожидать некоторой степени чистоты, когда у вас есть константная ссылка на объект функции. Например. если алгоритм требует чистой функции, потому что он может вызвать ее несколько раз и получить один и тот же результат. Кроме того, вы можете захотеть сохранить согласованность двух копий a и b. Когда оператор вызова функции является константой, вы можете разумно предположить, что использование функции a и использование функции b взаимозаменяемы.   -  person dyp    schedule 23.02.2014
comment
@dyp ах, хорошее замечание о чистоте, я должен был подумать об этом. Если вы можете написать это, я приму это как ответ и оставлю MutableLambda2 в качестве хорошо прокомментированного обходного пути для моей проблемы, пока Boost не догонит лямбда-выражения, захватывающие инициализацию.   -  person TemplateRex    schedule 23.02.2014


Ответы (2)


template<class L>
struct force_const_call_t {
  mutable L f;
  template<class...Args>
  auto operator()(Args&&...args) const
  { return f(std::forward<Args>(args)...); }
};
template<class L>
force_const_call_t<L> force_const_call(L&&f){
  return {std::forward<L>(f)};
}

приведенное выше должно позволить вам взять лямбду, обернуть ее в force_const_call( ... ) и вызвать ваш алгоритм boost без специального вызываемого объекта mutable (или, точнее, приведенное выше превращает лямбда-выражения в пользовательские вызываемые объекты mutable).

person Yakk - Adam Nevraumont    schedule 26.02.2014
comment
Ха! Я играл с обертыванием изменяемых лямбда-выражений внутри других лямбда-выражений, но это гениально благодаря своей простоте. - person TemplateRex; 27.02.2014
comment
Просто комментарий: он работает и даже работает эффективно (в моем приложении вся упаковка оптимизирована), но цена подделки константной подписи изменяемого объекта функции с отслеживанием состояния (как вашим решением, так и моим рукописным) заключается в том, что алгоритм не может предположим что-то более сильное, чем входные итераторы/однопроходные диапазоны. В противном случае зависимость от порядка, вызванная изменяемым состоянием, сбивает вас с толку. Пусть покупатель будет бдителен! - person TemplateRex; 03.03.2014
comment
@TemplateRex, что еще хуже, алгоритм может копировать состояние и оценивать разные части списка с разными копиями состояния. Этот вид обхода требований алгоритма требует, чтобы вы знали, что алгоритм написан неправильно (он не должен вызывать const вызовы), и в то же время вам не разрешено его переписывать. Вы можете сделать еще один шаг и написать оболочку, которая хранит лямбда-выражение в std::shared_ptr<L>, что означает, что копии теперь объекта общей функции используют одно и то же общее состояние, что может быть разумным. - person Yakk - Adam Nevraumont; 03.03.2014

Как указано в комментариях, изменяемая лямбда требует неконстантного оператора вызова функции, чтобы константные ссылки на объекты функций представляли чистые функции.

Оказывается, виновником моего приложения является Boost.Iterator, лежащий в основе реализации Boost.Range boost::adaptors::transformed. После некоторого изучения документации Boost.Iterator требования к transform_iterator, оказывается, что (выделено мной жирным шрифтом)

Тип UnaryFunction должен быть назначаемым, копируемым, а выражение f(*i) должно быть допустимым, где f — это константный объект типа UnaryFunction, i — объект типа Iterator, а тип f(*i) должен быть result_of<const UnaryFunction(iterator_traits<Iterator>::reference)>::type.

Поэтому нечистые функциональные объекты с состоянием не могут быть записаны с использованием лямбда-выражений, а вместо этого должны быть написаны с использованием const вызова функции operator() и с mutable элементами данных, представляющими состояние. Это также было отмечено в этих вопросах и ответах.

Примечание: для это.

person TemplateRex    schedule 23.02.2014