Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How is std::function constructed for a lambda

Tags:

I am a bit confused by how std::function is constructed at a given lambda. The constructor of std::function is listed here. Which one is actually used to capture a lambda? Is it template< class F > function( F f );? Looks like I can't construct std::function with a lambda that captures non-copy-constructible objects. Why is this necessary for lambda capture?

// fu is an object of type std::future 
std::function f = [future=std::move(fu)]() {...}  // compile error

// foo is an object of type int
std::function f = [foo=std::move(foo)]() {...} // compile ok
like image 700
Jes Avatar asked May 02 '16 14:05

Jes


2 Answers

The short answer is that the standard states only copyable function objects can be stored in a std::function. This is unsatisfactory: why?

std::function is a copyable type.

The standard states that when copied, it also copies its contents.

"But", you say, "I never copy it. Why should it need be copied?" An instance of std::function records how to copy its contents even if it never does so. It typically uses a technique known as type erasure.

Here is a toy example:

struct invoke_later {
  struct i_impl {
    virtual ~i_impl() {}
    virtual void invoke() const = 0;
    virtual std::unique_ptr<i_impl> clone() const = 0;
  };
  template<class T>
  struct impl:i_impl {
    T t;
    ~impl() = default;
    void invoke() const override {
      t();
    }
    impl(T&& tin):t(std::move(tin)) {}
    impl(T const& tin):t(tin) {}
    virtual std::unique_ptr<i_impl> clone() const {
      return std::make_unique<impl>(t);
    };
  };
  std::unique_ptr<i_impl> pimpl;

  template<class T,
    // SFINAE suppress using this ctor instead of copy/move ctors:
    std::enable_if_t< !std::is_same<std::decay_t<T>, invoke_later>{}, int>* =0
  >
  invoke_later( T&& t ):
    pimpl( std::make_unique<impl<std::decay_t<T>>( std::forward<T>(t) ) )
  {}

  invoke_later(invoke_later&&)=default;
  invoke_later(invoke_later const&o):
    pimpl(o.pimpl?o.pimpl->clone():std::unique_ptr<i_impl>{})
  {}

  ~invoke_later() = default;

  // assignment goes here

  void operator() const {
    pimpl->invoke();
  }
  explicit operator bool() const { return !!pimpl; }
};

the above is a toy example of a std::function<void()>.

The operation of copying is stored in the ->clone() method of the pimpl. It must be compiled even if you never call it.

The writers of the std specification where aware of the above technique, and knew its limitations, and wanted to permit std::functions to be implemented simply using it. In addition, they wanted simple operations on the std::function to behave in predictable ways: with non-copyable contents, what should copying the std::function do?

Note you can get around this issue by wrapping your state in a shared_ptr. Then copyies of your std::function will simply store shared references to your state, instead of copies.

template<class F>
auto shared_state( F&& f ) {
  return [pf = std::make_shared<std::decay_t<F>>(std::forward<F>(f))]
    (auto&&... args)->decltype(auto) {
      return (*pf)(decltype(args)(args)...);
    };
}

now:

std::function<Sig> f = shared_state([future=std::move(fu)]() {...});

will compile and work.

An alternative approach is to make a non-copyable std::function and use that instead of std::function.

Finally, whe working with future, a shared_future is a copyable future type and may be cheaper than doing a shared_state:

std::function<void()> f = [fu=fu.share()]{ /* code */ };
like image 89
Yakk - Adam Nevraumont Avatar answered Sep 28 '22 04:09

Yakk - Adam Nevraumont


A lambda that captures a move-only object by value becomes itself move-only, which makes sense since it contains said object.

But std::function has to be copy-constructible and copy-assignable, which means that it can only contain copiable objects.

like image 34
Quentin Avatar answered Sep 28 '22 04:09

Quentin