28

Consider the following code:

int x = 3;
auto f1 = [x]() mutable
{
    return x++;
};
auto f2 = [f1]()
{
    return f1();
};

This will not compile, because f1() is not const, and f2 is not declared as mutable. Does this mean that if I have a library function that accepts an arbitrary function argument and captures it in a lambda, I always need to make that lambda mutable, because I don't know what users can pass in? Notably, wrapping f1 in std::function seems to resolve this problem (how?).

riv
  • 6,846
  • 2
  • 34
  • 63
  • 1
    As a side note, that's the case with most of the Rust's functions, [where they explicitly accept `FnMut`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.fold) (even though in case of e.g. `fold` it would probably be called with `Fn` most of the time) – Bartek Banachewicz Apr 24 '19 at 09:30
  • Possible duplicate of [A const std::function wraps a non-const operator() / mutable lambda](https://stackoverflow.com/questions/47114936/a-const-stdfunction-wraps-a-non-const-operator-mutable-lambda) – rustyx Apr 24 '19 at 10:01
  • @rustyx thanks, that answers the last point, but not the whole question. – riv Apr 24 '19 at 10:36
  • 2
    A *"lambda violating const qualifiers"* sounds like something out of a sci-fi movie. – Malekai Apr 24 '19 at 17:21

2 Answers2

23

Does this mean that if I have a library function that accepts an arbitrary function argument and captures it in a lambda, I always need to make that lambda mutable, because I don't know what users can pass in?

That's a design decision for your library API. You can require client code to pass function objects with a const-qualified operator() (which is the case for non-mutable lambda expressions). If something different is passed, a compiler error is triggered. But if the context might require a function object argument that modifies its state, then yes, you have to make the internal lambda mutable.

An alternative would be to dispatch on the ability to invoke operator() on a const-qualified instance of the given function type. Something along those lines (note that this needs a fix for function objects with both const and non-const operator(), which results in an ambiguity):

template <class Fct>
auto wrap(Fct&& f) -> decltype(f(), void())
{
   [fct = std::forward<Fct>(f)]() mutable { fct(); }();
}

template <class Fct>
auto wrap(Fct&& f) -> decltype(std::declval<const Fct&>()(), void())
{
   [fct = std::forward<Fct>(f)]() { fct(); }();
}

Notably, wrapping f1 in std::function seems to resolve this problem (how?).

This is a bug in std::function due to its type-erasure and copy semantics. It allows non-const-qualified operator() to be invoked, which can be verified with such a snippet:

const std::function<void()> f = [i = 0]() mutable { ++i; };

f(); // Shouldn't be possible, but unfortunately, it is

This is a known issue, it's worth checking out Titus Winter's complaint on this.

lubgr
  • 37,368
  • 3
  • 66
  • 117
6

I'll start by addressing your second question first. std::function type erases, and holds a copy of the functor it's initialized with. That means there's a layer of indirection between std::function::operator() and the actual functor's operator().

Envision if you will, holding something in your class by pointer. Then you may call a mutating operation on the pointee from a const member function of your class, because it doesn't affect (in a shallow view) the pointer that the class holds. This is a similar situation to what you observed.

As for your first question... "Always" is too strong a word. It depends on your goal.

  1. If you want to support self mutating functors easily, then you should capture in a mutable lambda. But beware it may affect the library functions you may call now.

  2. If you wish to favor non-mutating operations, then a non-mutable lambda. I say "favor" because as we observed, the type system can be "fooled" with an extra level of indirection. So the approach you prefer is only going to be easier to use, not impossible to go around. This is as the sage advice goes, make correct use of your API easy, and incorrect harder.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • 1
    Notably, wrapping a passed lambda in `std::ref` provides the level of indirection that allows mutability even in `const` contexts. It also gets around the "functors may be copied around in the implementation" if you are after tracking some state. – Max Langhof Apr 24 '19 at 09:47