std::function 和 std::bind 的使用陷阱

std::functionstd::bindC++ 中非常常用的两个工具,然而要正确使用这两个工具还要更深入的理解。

最近写项目时遇到需要将不可复制构造的对象传给 std::bind 的情况,结果遇到了编译错误。代码逻辑可以抽象为下面这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <functional>

class Class {
public:
    Class() {}

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

    Class(Class &&) = default;
    Class &operator=(Class &&) = default;
};

void call(std::function<void()> func) {
    func();
}

int main() {
    auto func = std::bind([](Class &) { /* code */ }, Class());
    call(std::move(func));
    return 0;
}

这个错误 Language Server 是检测不到的,只有在编译后才能发现。编译错误信息如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
In file included from project.cpp:1:
In file included from /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/functional:59:
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/bits/std_function.h:159:10: error: call to implicitly-deleted copy constructor of 'std::_Bind<(lambda at project.cpp:19:27) (Class)>'
            new _Functor(*__source._M_access<const _Functor*>());
                ^        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/bits/std_function.h:196:8: note: in instantiation of member function 'std::_Function_base::_Base_manager<std::_Bind<(lambda at project.cpp:19:27) (Class)>>::_M_clone' requested here
              _M_clone(__dest, __source, _Local_storage());
              ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/bits/std_function.h:283:13: note: in instantiation of member function 'std::_Function_base::_Base_manager<std::_Bind<(lambda at project.cpp:19:27) (Class)>>::_M_manager' requested here
            _Base::_M_manager(__dest, __source, __op);
                   ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/bits/std_function.h:423:35: note: in instantiation of member function 'std::_Function_handler<void (), std::_Bind<(lambda at project.cpp:19:27) (Class)>>::_M_manager' requested here
              _M_manager = &_My_handler::_M_manager;
                                         ^
project.cpp:20:10: note: in instantiation of function template specialization 'std::function<void ()>::function<std::_Bind<(lambda at project.cpp:19:27) (Class)>, void, void>' requested here
    call(std::move(func));
         ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/functional:493:7: note: explicitly defaulted function was implicitly deleted here
      _Bind(const _Bind&) = default;
      ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/functional:412:29: note: copy constructor of '_Bind<(lambda at project.cpp:19:27) (Class)>' is implicitly deleted because field '_M_bound_args' has a deleted copy constructor
      tuple<_Bound_args...> _M_bound_args;
                            ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/tuple:744:17: note: explicitly defaulted function was implicitly deleted here
      constexpr tuple(const tuple&) = default;
                ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/tuple:599:19: note: copy constructor of 'tuple<Class>' is implicitly deleted because base class '_Tuple_impl<0, Class>' has a deleted copy constructor
    class tuple : public _Tuple_impl<0, _Elements...>
                  ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/tuple:435:17: note: explicitly defaulted function was implicitly deleted here
      constexpr _Tuple_impl(const _Tuple_impl&) = default;
                ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/tuple:408:7: note: copy constructor of '_Tuple_impl<0, Class>' is implicitly deleted because base class '_Head_base<0UL, Class>' has a deleted copy constructor
    : private _Head_base<_Idx, _Head>
      ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/tuple:86:17: note: explicitly defaulted function was implicitly deleted here
      constexpr _Head_base(const _Head_base&) = default;
                ^
/usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/11.2.0/../../../../include/c++/11.2.0/tuple:125:39: note: copy constructor of '_Head_base<0, Class, true>' is implicitly deleted because field '_M_head_impl' has a deleted copy constructor
      [[__no_unique_address__]] _Head _M_head_impl;
                                      ^
project.cpp:7:5: note: 'Class' has been explicitly marked deleted here
    Class(const Class &) = delete;
    ^
1 error generated.

这个编译信息具有一定的误导性,有可能首先会想到的是 std::bind 生成的函数对象不支持复制构造和移动构造,但实际上查看源码后发现,std::bind 返回一个 _Bind<_Signature> 类,其中一个特化为:

1
2
3
4
5
6
7
template <typename _Functor, typename... _Bound_args>
class _Bind<_Functor(_Bound_args...)> : public _Weak_result_type<_Functor> {
    _Functor _M_f;
    tuple<_Bound_args...> _M_bound_args;

    // ...
};

其中 _M_f 在这里是编译器将 lambda 表达式转换后的函数对象,复制构造和移动构造都可以支持,_M_bound_args 则是绑定的参数,使用 std::tuple 实现,其复制构造函数和移动构造函数均为 = default,所以至少移动构造函数也是可用的,也就是 std::bind 返回的这个函数对象 _Bind<_Signature> 也是可以移动构造的,因此 std::move(func) 是没有问题的。

所以问题出在 std::function 上,再查看 std::function 的源码,找到其构造函数对其他函数对象的重载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template <typename _Functor, typename = /* ... */, typename = /* ... */>
function(_Functor __f) : _Function_base() {
    typedef _Function_handler<_Res(_ArgTypes...), _Functor> _My_handler;

    if (_My_handler::_M_not_empty_function(__f)) {
        _My_handler::_M_init_functor(_M_functor, std::move(__f));
        _M_invoker = &_My_handler::_M_invoke;
        _M_manager = &_My_handler::_M_manager;
    }
}

如果 std::function 接受了一个函数对象,那么就会使用 _My_handler::_M_init_functor(_M_functor, std::move(__f)) 将该函数对象复制到自身内部的 _M_functor 成员上,而这个函数最终会调用以下两个函数之一:

1
2
3
4
5
6
7
static void _M_init_functor(_Any_data &__functor, _Functor &&__f, true_type) {
    ::new (__functor._M_access()) _Functor(std::move(__f));
}

static void _M_init_functor(_Any_data &__functor, _Functor &&__f, false_type) {
    __functor._M_access<_Functor *>() = new _Functor(std::move(__f));
}

事实上也只会调用以上这几个函数,这个过程也都是移动构造,理论上即使删除了复制构造函数也是可以正常工作的,其实问题出在其他函数使用了复制,比如下面这对:

1
2
3
4
5
6
7
static void _M_clone(_Any_data &__dest, const _Any_data &__source, true_type) {
    ::new (__dest._M_access()) _Functor(__source._M_access<_Functor>());
}

static void _M_clone(_Any_data &__dest, const _Any_data &__source, false_type) {
    __dest._M_access<_Functor *>() = new _Functor(*__source._M_access<const _Functor *>());
}

这里 __source._M_access<_Functor>() 显然不是右值,只能调用复制构造函数。而模板实例化是全部的,不是只对有使用到的代码进行处理。结论就是不可以用 std::function 保存不可复制构造的函数对象,包括这种 std::bind,因此解决方案也就是不使用 std::function。然而这样就不容易对函数签名进行限制,比如下面的这种方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <functional>

class Class {
public:
    Class() {}

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

    Class(Class &&) = default;
    Class &operator=(Class &&) = default;
};

template <typename Functor>
void call(Functor func) {
    func();
}

int main() {
    auto func = std::bind([](Class &) { /* code */ }, Class());
    call(std::move(func));
    return 0;
}
0%