【问题标题】:How does std::bind Results in calling the Copy Constructor Several Timesstd::bind 如何导致多次调用复制构造函数
【发布时间】:2021-08-24 16:39:50
【问题描述】:

我一直试图了解 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

我知道传递的参数将被复制,所以复制构造函数应该只被调用一次,但在上面的输出中复制构造函数被调用两次。为什么/如何发生这种情况?是否因为使用 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 时,为什么/如何调用复制构造函数三次?

版本 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)为什么/如何只调用一次复制构造函数?

版本 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)会发生什么,为什么/如何调用复制构造函数两次。

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

编辑:阅读 cmets 后,我还有一个问题/疑问:

问题 1

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

问题 2

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

问题 3

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

问题 4

如果我写auto callable = std::bind(&amp;NAME::f, 1_); callable(n); callable(std::ref(n)); 会怎样。声明callable(n) 是非法的吗?那么callable(std::ref(n));的声明呢。

【问题讨论】:

  • 这里的所有问题似乎都围绕着两个主题。 1. - std::function 的开销。 2. - 来自std::move的悲观情绪。
  • 附带说明:NAME::f() 需要一个指向对象的 NAME* 指针作为其 this 参数,而不是 NAME 对象本身。所以对std::bind() 的调用应该使用&amp;n。然后根本不会复制nstd::bind(&amp;NAME::f, &amp;n)
  • @RemyLebeau 是的,我已经知道如果我们在std::bind 中使用&amp;n 而不是n,那么根本不会复制。在这种情况下,我的问题是,如果我们像我一样通过 n 而不是 &amp;n,那么代码是否合法?
  • @JasonLiam 我想说不,如果 n 被复制,f() 将不会传递正确的 this 值。但情况似乎并非如此(除非这是依赖于编译器的行为):ideone.com/MxN29P。另一方面,en.cppreference.com/w/cpp/utility/functional/bind 说:“Callable 中所述,当调用指向非静态成员函数的指针或指向非静态数据成员的指针时,第一个参数必须是引用或指针(可能包括智能指针,例如std::shared_ptrstd::unique_ptr)指向将要访问其成员的对象。"

标签: c++ c++11 bind auto stdbind


【解决方案1】:

首先,根据rules for move constructors,没有为类NAME定义隐式移动构造函数。此外,来自注释here

如果只提供了复制构造函数,则所有参数类别 选择它(只要它引用 const,因为右值可以 绑定到 const 引用),这使得复制后备 移动,当移动不可用时。

因此,每当您使用std::move 时,您最终都会调用一个复制构造函数。这就解释了为什么与版本 3(分别是版本 1)相比,版本 4(分别是版本 2)对复制构造函数有额外的调用。

让我们看看剩下的复制构造函数。

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

当你声明时

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

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

最后,当你使用

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

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

其他问题的答案

1.

callable_1callable_2 的类型不同。 callable_2std::function 对象,而 callable_1unspecified type,是 std::bind 的结果。此外,它不是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 中的注释的严格解释

如 Callable 中所述,当调用指向非静态成员的指针时 函数或指向非静态数据成员的指针,第一个参数有 作为引用或指针(可能包括智能指针,例如 作为 std::shared_ptr 和 std::unique_ptr) 到一个对象,其成员将 被访问。

建议必须使用&amp;n 调用代码,而使用n 调用将是非法的。

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

如果 f 是指向类 T 的成员函数的指针:

a) 如果 std::is_base_of>::value 为真,则 INVOKE(f, t1, t2, ..., tN) 等价于 (t1 .*f)(t2, ..., tN)

b) 如果 std::decay_t 是 std::reference_wrapper,则 INVOKE(f, t1, t2, ..., tN) 等价 到 (t1.get().*f)(t2, ..., tN)

c) 如果 t1 不满足前面的 项,则 INVOKE(f, t1, t2, ..., tN) 等价于 ((*t1).*f)(t2, ..., tN)。

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

3.

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

除了这个问题,如果你改用std::ref 或者NAME::fconst,我看不出auto callable = [&amp;n]{n.f()};auto callable = std::bind(&amp;NAME::f, ref(n)); 之间的根本区别。关于这些,请通知that

绑定的参数被复制或移动,并且永远不会被传递 引用,除非包含在 std::ref 或 std::cref 中。

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

4.

reference for std::bindoperator() 下我们读到

如果存储的参数 arg 是 T 类型,那么 std::is_placeholder::value != 0 (意思是占位符,例如 std::placeholders::_1, _2, _3, ... 被用作 初始调用绑定),然后是占位符指示的参数 (u1 代表 _1,u2 代表 _2 等)被传递给可调用对象: 上面 std::invoke 调用中的参数 vn 是 std::forward(uj) 和 同一个调用中对应的类型Vn是Uj&&。

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

【讨论】:

  • 我还有一个问题:正如我在我的情况下所做的那样,如果我在std::bind 中通过n 而不是&amp;n,那么代码是否合法?例如,如果我写std::function&lt;void ()&gt; callable_1 = std::bind(&amp;NAME::f, n);auto callable_2 = std::bind(&amp;NAME::f, n); 那么这两个都是非法的吗?如果我在这两种情况下都通过ref(n) 而不是n,那么它们是非法的还是合法的?
  • 我已经编辑了我的问题(在编辑部分),您也可以回答剩下的问题吗?
【解决方案2】:
// 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; 

注意: 除了bind,你也可以使用lambda的

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

【讨论】:

  • 一个有效的观察,但我不认为这是对所提出问题的答案。这也许应该是一条评论。
  • 这是否意味着如果我使用auto callable_1 = std::bind(&amp;NAME::f, n);std::function&lt;void ()&gt; callable_2 = std::bind(&amp;NAME::f, n); 那么callable_1callable_2 的类型不同?如果是,那么自动为callable_1 推导出的类型是什么?还是说 callable_1 的 auto 推导出的类型会像 lambda 一样是一个未命名的类对象?
  • callable_1 将具有与 callable2 相同的签名,std::function 它将是不同的实例。 lambda 将是未命名的(它会生成一些奇怪的类型),但如果 lambda 的签名与 std::function 的签名匹配,您可以将其分配给 std::function。您可以让编译器从 lambda 推导出 std::function 类型,但它有点高级(我会为此添加另一个答案)
  • @JasonLiam std::bind() 不返回 std::function,而是一个未指定的实现定义类型,可分配给 std::function。所以是的,callable_1callable_2 会有不同的类型
【解决方案3】:
#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;
}

现在你可以像这样从 lambdas 制作 std::function

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

【讨论】:

  • 我在问题的“编辑”部分添加了更多问题。你也可以回答他们。您可以将它们附加到您之前的答案中。
  • 我没有足够的经验对 std::bind 创建的函数发表声明。我很久以前就改用 lambdas 了。不过,我确实对使用 ref 有一个警告。您必须确保在构造函数时引用的对象仍然存在(不管它是由 bind 还是 lambda 创建的)。所以传递本地(堆栈)变量可能会很棘手(当你将 lambdas 或带有 refs 的 std::functions 传递给其他线程时,事情会变得更加有趣)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多