我应该通过const-reference传递std :: function吗?


141

假设我有一个函数需要一个std::function

void callFunction(std::function<void()> x)
{
    x();
}

我应该x改为通过const-reference吗?:

void callFunction(const std::function<void()>& x)
{
    x();
}

这个问题的答案会根据功能的作用而变化吗?例如,如果它是一个类成员函数或构造函数,将其存储或初始化std::function为成员变量。


1
可能不是。我不确定,但我希望sizeof(std::function)不会超过2 * sizeof(size_t),这是您考虑使用const引用的最小大小。
Mats Petersson,

12
@Mats:我不认为std::function包装的大小比复制它的复杂性重要。如果涉及到深拷贝,它可能比sizeof建议的要昂贵得多。
Ben Voigt

您应该move加入功能吗?
Yakk-Adam Nevraumont

operator()()就是const这样一个const引用应该工作。但我从未使用过std :: function。
Neel Basu 2013年

@Yakk我只是将lambda直接传递给函数。
Sven Adbring

Answers:


79

如果需要性能,则在存储时按值传递。

假设您有一个名为“在UI线程中运行此函数”的函数。

std::future<void> run_in_ui_thread( std::function<void()> )

它在“ ui”线程中运行一些代码,然后future在完成时发出信号。(在UI线程应该用来弄乱UI元素的UI框架中很有用)

我们正在考虑两个签名:

std::future<void> run_in_ui_thread( std::function<void()> ) // (A)
std::future<void> run_in_ui_thread( std::function<void()> const& ) // (B)

现在,我们可能会如下使用它们:

run_in_ui_thread( [=]{
  // code goes here
} ).wait();

这将创建一个匿名闭包(lambda),从中构造出一个闭包std::function,将其传递给run_in_ui_thread函数,然后等待其完成在主线程中的运行。

在情况(A)中,std::function会直接从我们的lambda构造,然后在中使用run_in_ui_thread。λ move进入std::function,因此任何可移动状态都有效地携带到其中。

在第二种情况下,将std::function创建一个临时变量,即lambda为move放入其中,然后将该临时文件std::function用于内的引用run_in_ui_thread

到目前为止,还算不错-他们两个的表现相同。除了run_in_ui_thread要复制其函数参数以发送到ui线程执行之外!(它将在完成之前返回,因此它不能仅使用对其的引用)。对于情况(A),我们简单movestd::function其长期存储即可。在情况(B)中,我们被迫复制std::function

那家商店使价值传递更加优化。如果有可能,您将存储的副本std::function,并按值传递。否则,这两种方法都大致等效:按值的唯一缺点是,如果您使用的是相同的笨重对象,std::function并且又有一个子方法使用另一个子方法,则该方法不可行。除非,move否则a的效率将与a相同const&

现在,如果我们在 std::function

假设std::function存储一些对象operator() const,但是它也具有一些mutable可以修改的数据成员(多么粗鲁!)。

在这种std::function<> const&情况下mutable修改后数据成员将传播到函数调用之外。在这种std::function<>情况下,他们不会。

这是一个相对奇怪的极端情况。

您想要std::function像对待其他任何重量轻,价格便宜的移动设备一样对待。移动很便宜,复制可能很昂贵。


如您所说,“按值传递如果存储它”的语义优势是,按合同,函数不能保持传递的参数地址。但是,“除非这样,举动会像const&一样有效”,这是真的吗?我总是看到复制操作的成本加上移动操作的成本。通过时,const&我只看到复制操作的成本。
ceztko

2
@ceztko在(A)和(B)情况下,std::function都是从lambda创建临时文件的。在(A)中,临时变量被忽略到中run_in_ui_thread。在(B)中,对所述临时引用被传递给run_in_ui_thread。只要您的std::functions是从作为临时对象的lambda创建的,则该子句成立。上一段处理std::function持久化的情况。如果我们存储,则仅从lambda创建,function const&function具有完全相同的开销。
Yakk-亚当·内夫罗蒙特

啊,我明白了!当然,这取决于在之外发生的事情run_in_ui_thread()。是否只有签名说“通过引用,但我不会存储地址”?
ceztko

@ceztko不,没有。
Yakk-亚当·内夫罗蒙特

1
@ Yakk-AdamNevraumont是否更完整,以涵盖通过右值传递的另一种选择:std::future<void> run_in_ui_thread( std::function<void()>&& )
Pavel P

33

如果您担心性能,并且没有定义虚拟成员函数,则很可能根本不使用它std::function

将functor类型设置为模板参数比可以允许更大的优化std::function,包括内联functor逻辑。这些优化的效果可能会大大超过copy-vs-indirect有关如何传递的关注std::function

快点:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}

1
实际上,我完全不担心性能。我只是认为在常用的地方使用const-references是很常见的做法(想到字符串和向量)。
Sven Adbring

13
@Ben:我认为实现此目标的最现代的嬉皮友好方式是使用std::forward<Functor>(x)();,以保留函子的值类别,因为它是“通用”参考。不过,在99%的情况下不会有所作为。
GManNickG

1
@Ben Voigt,对于您的情况,我是否需要搬动调用该函数?callFunction(std::move(myFunctor));
arias_JC

2
@arias_JC:如果参数是lambda,则它已经是一个右值。如果您有左值,则可以使用它(std::move如果不再需要其他任何方式),或者如果不想移出现有对象,则直接传递。引用折叠规则确保callFunction<T&>()的参数类型为T&,而不是T&&
Ben Voigt

1
@BoltzmannBrain:我选择不进行更改,因为它仅在最简单的情况下才有效,该函数仅被调用一次。我的答案是“我应该如何传递功能对象”的问题。并且不限于除了无条件调用该函子一次外什么都不做的函数。
Ben Voigt

25

与C ++ 11中一样,按值/引用/常量引用进行传递取决于您对参数的处理方式。std::function没什么两样

按值传递允许您将参数移动到变量(通常是类的成员变量)中:

struct Foo {
    Foo(Object o) : m_o(std::move(o)) {}

    Object m_o;
};

当您知道函数将移动其参数时,这是最好的解决方案,您的用户可以通过这种方式控制他们调用函数的方式:

Foo f1{Object()};               // move the temporary, followed by a move in the constructor
Foo f2{some_object};            // copy the object, followed by a move in the constructor
Foo f3{std::move(some_object)}; // move the object, followed by a move in the constructor

我相信您已经知道(非)const引用的语义,因此我不会感到困惑。如果您需要我添加更多有关此的解释,请询问,我会进行更新。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.