std::bind 如何导致多次调用复制构造函数

How does std::bind Results in calling the Copy Constructor Several Times

我一直在努力理解 std::bind 是如何工作的。所以处理不同的例子。下面是我无法理解其输出的示例程序。

版本 1

class NAME
{
  public:
    void f()
    {
        std::cout<<"f"<<std::endl;
    }
    NAME()
    {
        std::cout<<"default constructor"<<std::endl;
    }
    NAME(const NAME&)
    {
        std::cout<<"copy constructor"<<std::endl;
    }
};
int main()
{
   std::cout << "Hello World" << std::endl; 
   NAME n;
   std::function<void ()> callable = std::bind(&NAME::f, n);
   
   
   return 0;
}

上面版本1的输出结果如下:

Hello World
default constructor
copy constructor
copy constructor

我知道传递的参数会被复制,所以复制构造函数应该只被调用一次但是在上面的输出中复制构造函数被调用了两次。 Why/how 会发生这种情况吗?是因为使用 std::bind 创建的新可调用对象将用于在 lhs 上使用 std::function 初始化另一个可调用对象吗?

版本 2

int main()
{
   std::cout << "Hello World" << std::endl; 
   NAME n;
   std::function<void ()> callable = std::move(std::bind(&NAME::f, n));
   return 0;
}

VERSION 2的输出如下:

Hello World
default constructor
copy constructor
copy constructor
copy constructor

在上面的输出中(对于版本 2),当我使用 std::move 时,why/how 复制构造函数被调用了三次?

版本 3

int main()
{
   std::cout << "Hello World" << std::endl; 
   NAME n;
   auto callable = std::bind(&NAME::f, n);
   return 0;
}

版本3的输出如下:

Hello World
default constructor
copy constructor

在这种情况下(版本 3)why/how 复制构造函数是否只调用一次?

版本 4

int main()
{
   std::cout << "Hello World" << std::endl; 
   NAME n;
   auto callable = std::move(std::bind(&NAME::f, n));
   return 0;
}

版本4的输出如下:

Hello World
default constructor
copy constructor
copy constructor

当我们使用 autostd::move() 时,在这种情况下(版本 4)会发生什么,why/how 是复制构造函数被调用了两次。

PS:程序在在线编译器上执行:Online compiler Used

编辑: 阅读评论后,我有进一步的 question/doubt:

问题一

如果我使用 auto callable_1 = std::bind(&NAME::f, n);std::function<void ()> callable_2 = std::bind(&NAME::f, n); 那么 callable_1callable_2 的类型是否不同?如果是,那么 autocallable_1 推导出的类型是什么?还是 autocallable_1 推导的类型将是一个未命名的 class 对象,就像 lambda.

问题二

我还有一个问题:正如我在我的案例中所做的那样,如果我在 std::bind 中传递 n 而不是 &n 那么代码是否合法?例如,如果我写 std::function<void ()> callable_1 = std::bind(&NAME::f, n);auto callable_2 = std::bind(&NAME::f, n); 那么这两个都是非法的吗?如果我在这两种情况下都通过 ref(n) 而不是 n 怎么办,那么它们是非法的还是合法的?

问题3

语句 auto callable = [&n]{n.f()};auto callable = std::bind(&NAME::f, cref(n)); 是否等价(在功能上),也许在其他方面是否相同,除了编码风格之外,是否有任何理由(优势)选择一个而不是另一个?

问题4

如果我写 auto callable = std::bind(&NAME::f, 1_); callable(n); callable(std::ref(n)); 会怎样? callable(n) 语句是否非法?那么语句 callable(std::ref(n));.

呢?
// default constructor will be called
NAME n; 

// bind makes a copy of n into an internal struct, copy constructor of n is called.
auto fn = std::bind(&NAME::f, n); 

// assignment operator makes a copy of members of fn to members of std::function. 
// This is also a copy constructor call of NAME
std::function<void ()> callable = fn; 

注意: 除了绑定,您还可以使用 lambda 的

std::function<void ()> callable([&n]{n.f()});
#pragma once

namespace details
{
    template<typename T>
    struct memfun_type
    {
        using type = void;
    };

    template<typename RetvalType, typename ClassType, typename... Args>
    struct memfun_type<RetvalType(ClassType::*)(Args...) const>
    {
        using type = typename std::function<RetvalType(Args...)>;
    };

} // details

template<typename Fn>
typename details::memfun_type<decltype(&Fn::operator())>::type 
make_std_function(Fn const& fn)
{
    return fn;
}

现在您可以像这样从 lambda 中创建 std::function

auto std_fn = make_std_function([](int n){return 2*n;});
int answer = std_fn(2);

首先,根据rules for move constructors, no implicit move constructor is defined for the class NAME. Further, from the Notes here

If only the copy constructor is provided, all argument categories select it (as long as it takes a reference to const, since rvalues can bind to const references), which makes copying the fallback for moving, when moving is unavailable.

因此,无论何时使用 std::move,您最终都会调用复制构造函数。这就解释了为什么版本 4(分别是版本 2)比版本 3(分别是版本 1)多了一个拷贝构造函数调用。

让我们看看剩下的拷贝构造函数。

正如您正确指出的那样,通过传递 std::bind 的第二个参数调用复制构造函数。此帐号为所有版本中的第一个调用。

当你申报时

 std::function<void ()> callable = std::bind(&NAME::f, n);

您正在调用 std::function 的构造函数,传递一个参数 std::bind(&NAME::f, n),然后再次复制该参数。这说明了版本 1 的复制构造函数的第二次调用,以及版本 2 中的第三次调用。请注意 mandatory copy elision 不适用于此处,因为您没有传递 std::function 对象。

最后,当你使用

auto callable = std::bind(...)

您正在声明一个未命名类型的变量,其中包含调用 std::bind 的结果。 声明中不涉及副本。这就是为什么版本 3 与版本 1 相比少了一次对复制构造函数的调用。

附加问题的答案

1.

callable_1callable_2的类型不同。 callable_2 是一个 std::function 对象,而 callable_1 是一个 unspecified type, the result of std::bind. Also, it is not a lamda。要查看此内容,您可以 运行 像

这样的代码
   auto callable_1 = std::bind(&NAME::f, n);
   std::function<void ()> callable_2 = std::bind(&NAME::f, n);
   // a generic lambda
   auto callable_3 = [&]() { n.f(); };
   std::cout << std::boolalpha;
   std::cout << std::is_bind_expression<decltype(callable_1)>::value << std::endl;
   std::cout << std::is_bind_expression<decltype(callable_2)>::value << std::endl;
   std::cout << std::is_bind_expression<decltype(callable_3)>::value << std::endl;

看到了live on Coliru.

2.

如@RemyLebeau 所述,对 reference of std::bind

中注释的严格解释

As described in Callable, when invoking a pointer to non-static member function or pointer to non-static data member, the first argument has to be a reference or pointer (including, possibly, smart pointer such as std::shared_ptr and std::unique_ptr) to an object whose member will be accessed.

会建议代码必须用 &n 调用,用 n 调用是非法的。

但是,调用 operator() 会导致 std::invoke。从 reference of std::invoke 我们读到(我稍微重新格式化):

If f is a pointer to member function of class T:

a) If std::is_base_of<T, std::decay_t<decltype(t1)>>::value is true, then INVOKE(f, t1, t2, ..., tN) is equivalent to (t1.*f)(t2, ..., tN)

b) If std::decay_t<decltype(t1)> is a specialization of std::reference_wrapper, then INVOKE(f, t1, t2, ..., tN) is equivalent to (t1.get().*f)(t2, ..., tN)

c) If t1 does not satisfy the previous items, then INVOKE(f, t1, t2, ..., tN) is equivalent to ((*t1).*f)(t2, ..., tN).

据此,调用 std::bindn(案例 a))或 &n(案例 c))应该是等价的(除了额外的副本,如果你使用 n),因为 std::decay_t<decltype(n)> 给出 NAMEstd::is_base_of<NAME, NAME>::valuetrue(参见 reference for std::is_base_of)。 传递 ref(n) 对应于情况 b),因此它应该是正确的并且等同于其他情况(除了上面讨论的副本)。

3.

请注意 cref 为您提供了 const NAME& 的引用包装器。所以你将无法调用 callable 因为 NAME::f 不是 const 成员函数。事实上,如果你添加一个 callable(); 代码 does not compile.

除了这个问题,如果你改用 std::ref 或者如果 NAME::fconst,我看不出 auto callable = [&n]{n.f()}; 和 [=51] 之间的根本区别=].考虑到这些,请注意 that:

The arguments to bind are copied or moved, and are never passed by reference unless wrapped in std::ref or std::cref.

就个人而言,我发现 lambda 语法更清晰。

4.

来自reference for std::bind,在operator()下我们读到

If the stored argument arg is of type T, for which std::is_placeholder::value != 0 (meaning, a placeholder such as std::placeholders::_1, _2, _3, ... was used as the argument to the initial call to bind), then the argument indicated by the placeholder (u1 for _1, u2 for _2, etc) is passed to the invokable object: the argument vn in the std::invoke call above is std::forward(uj) and the corresponding type Vn in the same call is Uj&&.

因此,使用占位符的效果又回到了问题1的情况。代码确实compiles不同的编译器。