Home > Software engineering >  c 20: how to move capture a class instance
c 20: how to move capture a class instance

Time:08-10

I want to construct a factory function for a class B, which needs a callback. The factory function gives a lambda to B, but this lambda needs an instance of another class A, which I want to create inside of my factory and move into the lambda. A is not copyable but moveable, so I would expect that this should be possible.

Unexpectedly, the compiler wants to use the deleted copy constructor of A rather than its move constructor (error: use of deleted function 'A::A(const A&)').

Minimum example:

#include <functional>

class A {
 public:
    A() {}

    A(const A &) = delete;
    A& operator=(const A &) = delete;

    A(A&&) noexcept {}
};

class B {
 public:
    explicit B(std::function<void()> cb) { (void)(cb); };
};

B b_factory() {
    auto a { A() };
    return B {
        [a = std::move(a)]() {
            // do something with a
        }
    };
}

B b = b_factory();

Compiler output:

$ arm-poky-linux-gnueabi-g   --version
arm-poky-linux-gnueabi-g   (GCC) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ arm-poky-linux-gnueabi-g   -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 -fstack-protector-strong  -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security -Werror=format-security --sysroot=/home/.../poky_sdk_toolchain/sysroots/cortexa9hf-neon-poky-linux-gnueabi --sysroot=/home/.../poky_sdk_toolchain/sysroots/cortexa9hf-neon-poky-linux-gnueabi   -I../include -O2 -pipe -g -feliminate-unused-debug-types  -g   -Wall -Wextra -Werror -Wno-psabi -std=gnu  2a -MD -MT src/lib/CMakeFiles/traplog.dir/move_example.cpp.o -MF src/lib/CMakeFiles/traplog.dir/move_example.cpp.o.d -o src/lib/CMakeFiles/traplog.dir/move_example.cpp.o -c ../src/lib/move_example.cpp
In file included from /home/.../poky_sdk_toolchain/sysroots/cortexa9hf-neon-poky-linux-gnueabi/usr/include/c  /9.3.0/functional:59,
                 from ../src/lib/move_example.cpp:1:
/home/.../poky_sdk_toolchain/sysroots/cortexa9hf-neon-poky-linux-gnueabi/usr/include/c  /9.3.0/bits/std_function.h: In instantiation of 'static void std::_Function_base::_Base_manager<_Functor>::_M_clone(std::_Any_data&, const std::_Any_data&, std::false_type) [with _Functor = b_factory()::<lambda()>; std::false_type = std::integral_constant<bool, false>]':
/home/.../poky_sdk_toolchain/sysroots/cortexa9hf-neon-poky-linux-gnueabi/usr/include/c  /9.3.0/bits/std_function.h:211:16:   required from 'static bool std::_Function_base::_Base_manager<_Functor>::_M_manager(std::_Any_data&, const std::_Any_data&, std::_Manager_operation) [with _Functor = b_factory()::<lambda()>]'
/home/.../poky_sdk_toolchain/sysroots/cortexa9hf-neon-poky-linux-gnueabi/usr/include/c  /9.3.0/bits/std_function.h:677:19:   required from 'std::function<_Res(_ArgTypes ...)>::function(_Functor) [with _Functor = b_factory()::<lambda()>; <template-parameter-2-2> = void; <template-parameter-2-3> = void; _Res = void; _ArgTypes = {}]'
../src/lib/move_example.cpp:27:5:   required from here
/home/.../poky_sdk_toolchain/sysroots/cortexa9hf-neon-poky-linux-gnueabi/usr/include/c  /9.3.0/bits/std_function.h:176:6: error: use of deleted function 'b_factory()::<lambda()>::<lambda>(const b_factory()::<lambda()>&)'
  176 |      new _Functor(*__source._M_access<const _Functor*>());
      |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../src/lib/move_example.cpp:24:26: note: 'b_factory()::<lambda()>::<lambda>(const b_factory()::<lambda()>&)' is implicitly deleted because the default definition would be ill-formed:
   24 |         [a = std::move(a)]() {
      |                          ^
../src/lib/move_example.cpp:24:26: error: use of deleted function 'A::A(const A&)'
../src/lib/move_example.cpp:8:5: note: declared here
    8 |     A(const A &) = delete;
      |     ^

Can somebody explain this?

  • edit: added full build output

CodePudding user response:

IMO, the reason is that the type lambda you passed to std::function ctor violate std::function type requirement, which needs the passed Callable must be CopyConstructible. Since the captured type A's copy constructor is deleted, it results that the lambda's copy constructor cannot be implicitly-declared.

Here's some reference from cppreference page:

  1. Initializes the target with std::forward<F>(f). The target is of type std::decay<F>::type. If f is a null pointer to function, a null pointer to member, or an empty value of some std::function specialization, *this will be empty after the call. This constructor does not participate in overload resolution unless the target type is not same as function, and its lvalue is Callable for argument types Args... and return type R. The program is ill-formed if the target type is not copy-constructible or initialization of the target is ill-formed.

Also from the same page:

std::decay<F>::type must meet the requirements of Callable and CopyConstructible.


Possible solution 1

If c 23 is acceptable to you, you can directly use std::move_only_function. https://godbolt.org/z/qhfd83955

Possible solution 2

You can write a custom naive function wrapper to handle your move only type, which is a simple type-erasure class with a function call operator, basically the same thing as chapter 9 of book "C Concurrency In Action":

class TaskWrapper {
  struct impl_base {
    virtual void call() = 0;
    virtual ~impl_base() = default;
  };
  std::unique_ptr<impl_base> impl;
  template <typename F>
  struct impl_type : impl_base {
    F f;
    impl_type(F&& f_) : f(std::move(f_)) {}
    void call() final { f(); }
  };

 public:
  template <typename F>
  TaskWrapper(F&& f) : impl(new impl_type<F>(std::forward<F>(f))) {}
  void operator()() { impl->call(); }
  TaskWrapper() = default;
  TaskWrapper(TaskWrapper&& other) noexcept : impl(std::move(other.impl)) {}
  TaskWrapper& operator=(TaskWrapper&& other) noexcept {
    impl = std::move(other.impl);
    return *this;
  }
  TaskWrapper(const TaskWrapper&) = delete;
  TaskWrapper(TaskWrapper&) = delete;
  TaskWrapper& operator=(const TaskWrapper&) = delete;
};

And change B signature to:

class B {
 public:
    explicit B(TaskWrapper cb) { (void)(cb); };
};

Then use it like:

B b_factory() {
    auto a { A() };
    return B {
        TaskWrapper([a = std::move(a)]() {
            // do something with a
        })
    };
}
  • Related