Question

This question is related to this previous one where it was noticed that init-capturing mutable lambdas are incompatible with Boost's range and iterator transform for some rather obscure and deeply nested typedef failures that may or may not be easy to resolve through hacking the Boost.Range sources.

The accepted answer suggested storing the lambda in a std::function object. To avoid potential virtual function call overhead, I wrote two function objects that could serve as potential work-arounds. They are called MutableLambda1 and MutableLambda2 in the code below

#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!   */
}

Live Example that won't compile the lines with lam and MutableLambda1, and correctly prints 1, 4, 9, 16 for the line with MutableLambda2.

However, the draft Standard mentions

5.1.2 Lambda expressions [expr.prim.lambda]

5 [...] This function call operator or operator template is declared const (9.3.1) if and only if the lambda-expression’s parameter-declaration-clause is not followed by mutable. [...]

11 For every init-capture a non-static data member named by the identifier of the init-capture is declared in the closure type. This member is not a bit-field and not mutable. [...]

This means that MutableLambda2 is not a conforming handwritten replacement for an init-capturing mutable lambda expression.

Questions

  • why is the implementation of init-capturing mutable lambdas the way it is (i.e. non-const function call operator)?
  • why is the seemingly equivalent alternative of mutable data members with a const function call operator forbidden?
  • (bonus) why does the Boost range and iterator transform rely on the fact that a function objects operator() is const?
Was it helpful?

Solution

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)};
}

the above should let you take a lambda, wrap it in force_const_call( ... ), and call your boost algorithm, without a custom mutable callable object (or more precisely, the above turns lambdas into custom mutable callables).

OTHER TIPS

As pointed out in the comments, a mutable lambda requires a non-const function call operator in order to let const references to function objects represent pure functions.

It turns out that the culprit for my application is Boost.Iterator underyling the Boost.Range implementation of boost::adaptors::transformed. After some digging in the Boost.Iterator documentation's requirements for transform_iterator, it turns out that (bold emphasis mine)

The type UnaryFunction must be Assignable, Copy Constructible, and the expression f(*i) must be valid where f is a const object of type UnaryFunction, i is an object of type Iterator, and where the type of f(*i) must be result_of<const UnaryFunction(iterator_traits<Iterator>::reference)>::type.

Stateful non-pure function objects can therefore not be written using lambdas but instead have to written using a const function call operator() and with mutable data members representing the state. This was also remarked in this related Q&A.

Note: there is an open bug report for this.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top