我应该通过const引用传递std::function吗?

192
假设我有一个函数,它接受一个`std::function`作为参数:
void callFunction(std::function<void()> x)
{
    x();
}

我应该通过const引用传递x吗?
void callFunction(const std::function<void()>& x)
{
    x();
}

这个问题的答案是否取决于函数对它的处理方式?例如,如果它是一个成员函数或构造函数,将std::function存储或初始化为数据成员。

1
可能不会。我不确定,但我预计sizeof(std::function)不会超过2 * sizeof(size_t),这是您考虑常量引用的最小大小。 - Mats Petersson
13
@Mats:我认为std::function的包装器大小并不像复制它的复杂度那样重要。如果涉及深拷贝,它可能比sizeof所建议的更昂贵。 - Ben Voigt
你应该将这个函数移进来吗? - Yakk - Adam Nevraumont
@Yakk 我刚刚直接将一个 lambda 函数传递给了这个函数。 - Sven Adbring
@BenVoigt:但是有什么可以复制的吗?std::function本身并不保存类或类的副本,对吧? - Mats Petersson
显示剩余5条评论
3个回答

100

如果您希望获得更好的性能,当您需要存储数据时,请使用按值传递。

假设您有一个名为“在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内部使用。lambda被移动到std::function中,因此任何可移动的状态都可以高效地传递到其中。

在第二种情况下,创建了一个临时的std::function,然后将lambda移动到其中,然后通过引用在run_in_ui_thread中使用该临时std::function

目前为止,两者表现相同。除了run_in_ui_thread将复制其函数参数以发送到ui线程执行!(它将返回之前,所以不能仅使用对它的引用)。对于情况(A),我们只需将std::function移动到其长期存储中即可。在情况(B)下,我们被迫复制std::function

该存储使按值传递更加优化。如果有可能存储std::function的副本,则按值传递更优。否则,两种方式大致相同:唯一的缺点是按值传递的情况下,如果您正在使用相同的庞大std::function并让一个子方法接着另一个使用它。除此之外,moveconst&一样有效。

现在,如果在std::function中存在持久状态,则两者之间还有一些其他差异。

假设std::function存储一些带有operator() const的对象,但它也具有一些可变数据成员,而它会对其进行修改(多么无礼啊!)。

std::function<> const&情况下,修改的可变数据成员将传播到函数调用之外。在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::function是从lambda作为临时变量创建的,那么该条款就成立。上一段讨论了std::function持久存在的情况。如果我们不存储,只是从lambda创建,那么function const&function具有完全相同的开销。 - Yakk - Adam Nevraumont
@ceztko 不,没有。 - Yakk - Adam Nevraumont
3
@Yakk-AdamNevraumont 如果覆盖另一种通过rvalue ref传递的选项会更完整:std::future<void> run_in_ui_thread( std::function<void()>&& ) - Pavel P
1
@passionateProgrammer 迟做总比不做好。https://dev59.com/fWMl5IYBdhLWcg3wXWET - Yakk - Adam Nevraumont
显示剩余2条评论

38

如果你担心性能,并且你没有定义虚成员函数,那么你很可能根本不应该使用std::function

将函数对象类型作为模板参数允许进行比std::function更大程度的优化,包括内联函数对象逻辑。这些优化的效果很可能大大超过如何传递std::function时的复制和间接性考虑。

更快:

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

2
实际上,我一点也不担心性能问题。我只是认为在应该使用const引用的地方使用它们是常见做法(比如字符串和向量)。 - Sven Adbring
17
我认为最现代的嬉皮友好实现方式是使用std::forward<Functor>(x)();,以保留函数对象的值类别,因为它是“通用”引用。虽然这在99%的情况下不会有任何影响。 - GManNickG
1
@Ben Voigt,针对您的情况,我需要使用move调用函数吗?callFunction(std::move(myFunctor)); - arias_JC
2
@arias_JC:如果参数是lambda,它已经是rvalue了。如果你有一个lvalue,你可以使用std::move,如果你不再需要它,或者直接传递,如果你不想移动现有对象。引用折叠规则确保callFunction<T&>()的参数类型为T&,而不是T&& - Ben Voigt
1
@BoltzmannBrain:我选择不进行更改,因为它仅适用于最简单的情况,即函数仅被调用一次。我的回答是针对“如何传递函数对象?”这个问题,并不限于一个除了无条件地恰好调用该函数对象一次之外什么都不做的函数。 - Ben Voigt
显示剩余3条评论

34

像在C++11中一样,按值/引用/const引用传递取决于您对参数的操作。 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

我相信你已经了解(非)常量引用的语义,所以我不再赘述。如果你需要我添加更多关于这个的解释,请告诉我,我会进行更新。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接