5
\$\begingroup\$

I'd like to emplace a lambda which captured a unique-pointer into a container, but it failed to compile under GCC 7.3.0 as C++17. From the error messages, it's calling the copy constructor of unique_ptr when constructing function.

compile

#include <memory>
#include <functional>
#include <string>
#include <queue>

int main (){
    auto ptr = std::make_unique<std::string>("123456");
    auto l = [p = std::move(ptr)] () mutable {
        p.reset();
    };
    std::queue<std::function<void()>> q;
    q.emplace(std::move(l));
    return 0;
}

Actually, I have many lambdas with the same parameter list and same return type, and I need to emplace them to one container. So I need a common type to wrap, like std::function<void()>.

In addition, I don't want add something else, like using std::shared_ptr to make it copyable, or using another lambda to wrap again.

Based on the above, I designed LambdaWrapper to use instead of std::function. The variable which captured won't be copied, because of the non-copyable LambdaWrapper.

class LambdaWrapper {
 public:
  template <class T, typename = std::is_invocable<decltype(std::mem_fn(&T::operator()))>>
  LambdaWrapper(T&& lambda) {
    MemberFunction memfn;
    // This forced type conversion is refers to std::_Function_base::_Base_manager::_M_init_functor
    new (&memfn)(decltype(&T::operator()))(&T::operator());
    // Member function pointer is 16B. The first 8B is the address of function, and the last 8B is the offset of this pointer. Because the lambda type is not inherited from other classes, the offset is 0 and it can be ignored.
    new (&func_) void*(memfn.func);
    new (&deleter_)(void (*)(T*))(&DeleterImpl<T>::f);
    data_ = malloc(sizeof(T));
    new (data_) T(std::forward<T>(lambda));
  }
  ~LambdaWrapper() {
    if (data_ != nullptr && deleter_ != nullptr) {
      deleter_(data_);
    } else if (data_ != nullptr || deleter_ != nullptr) {
      // something wrong
    }
  }
  void operator()() { func_(data_); }

  LambdaWrapper(LambdaWrapper&& T) {
    data_ = T.data_;
    func_ = T.func_;
    deleter_ = T.deleter_;
    T.data_ = nullptr;
    T.func_ = nullptr;
    T.deleter_ = nullptr;
  }

  LambdaWrapper& operator=(LambdaWrapper&& T) {
    data_ = T.data_;
    func_ = T.func_;
    deleter_ = T.deleter_;
    T.data_ = nullptr;
    T.func_ = nullptr;
    T.deleter_ = nullptr;
    return *this;
  }

 private:
  LambdaWrapper(const LambdaWrapper&) = delete;
  LambdaWrapper& operator=(const LambdaWrapper&) = delete;
  struct MemberFunction {
    void* func;
    void* ptr;
  };
  void (*func_)(void*);
  void (*deleter_)(void*);
  void* data_;
  template <class T>
  struct DeleterImpl {
    static void f(T* t) { delete t; }
  };
};

Finally, is there any better solution? And is there any problem with LambdaWrapper?

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Welcome to CodeReview. Please try to stick to use english only. \$\endgroup\$ Mar 25, 2021 at 7:33
  • \$\begingroup\$ Interesting. The LambdaWrapper<> looks suspiciously like a unique_ptr<>. Maybe it should be called unique_function<>? \$\endgroup\$
    – G. Sliepen
    Mar 25, 2021 at 21:24
  • \$\begingroup\$ Ah, this might be a duplicate of stackoverflow.com/questions/25330716/…. Also, it seems a std::unique_function<> was proposed, but it's not in C++20: open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0228r3.html \$\endgroup\$
    – G. Sliepen
    Mar 25, 2021 at 21:26
  • \$\begingroup\$ To me, it looks like a std::function without the requirement to be copy-constructible or copy-assignable. Which is something I had naively assumed that the standard function would be when wrapping a move-only type. \$\endgroup\$ Mar 26, 2021 at 13:02
  • 2
    \$\begingroup\$ @G.Sliepen, "duplicate" is probably the wrong word there, as this is Code Review. "A solution to" might be more accurate. \$\endgroup\$ Mar 26, 2021 at 13:04

1 Answer 1

2
\$\begingroup\$

Missing includes - the code won't compile until I prepend

#include <functional>
#include <stdlib.h>
#include <type_traits>

(though I would recommend <cstdlib> and use the proper namespace for std::malloc()).

This comment is non-portable and potentially misleading:

// Member function pointer is 16B. The first 8B is the address of function, and the last 8B is the offset of this pointer. Because the lambda type is not inherited from other classes, the offset is 0 and it can be ignored.

Sizes of pointer types are implementation-dependent, and the structure of a member function pointer is entirely unspecified. This decomposition is entirely compiler-specific, so should be guarded to prevent hard-to-diagnose errors.

data_ is created using std::malloc() but released using delete. That's a definite bug, and shows up immediately when exercised under Valgrind.

The operator() looks broken - it only allows use of a no-args function, and discards the result. I would have expected something more like

template<typename... Args>
std::invoke_result<T, Args...>
operator()(Args&&... args);

The constructors probably ought to initialise members where possible, rather than assigning. Consider writing a swap() member function, and using that to implement the move operations.

Overall, I'm disappointed that we don't have something that's much more like std::function<>, given that we'd like to use it in a similar way, only with move-only callables.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.