Search code examples
c++lambdalanguage-lawyermutablec++14

Why can't init-capturing mutable lambdas have mutable data members?


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?

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).