使用std::function的移动语义

29

std::function提供了一个从右值引用构造函数。 根据标准,移动的函数对象会发生什么?它会变成空的,这样再次调用它就没有任何效果吗?


这取决于类的移动复制构造函数和/或移动赋值运算符。 - juanchopanza
@juanchopanza 你是什么意思?当然这取决于移动构造函数。它对原始函数对象做了什么?移动后会变为空吗? - Martin
抱歉,我以为你是指通过绑定对象传递可变参数构造函数的参数。 - juanchopanza
4个回答

27

这个问题非常令人困惑。我将尝试清晰地阐述...

本节描述了std定义对象的移动来源状态:

17.6.5.15 [lib.types.movedfrom]

在C++标准库中定义的类型的对象可能被移动(12.8)。移动操作可以明确指定或隐式生成。除非另有规定,这样的移动源自对象将放置在有效但未指定的状态。

这是什么意思?这意味着,给定一个std定义的移动来源对象,您可以对该对象进行任何不需要先验知识的操作。不需要先验知识的当前状态的类别是那些没有前提条件的操作。

例如,您可以在已移动的vector上调用clear(),因为vector::clear()没有前提条件。但是您不能调用pop_back(),因为它具有前提条件。

具体看function的调用运算符:

20.8.11.2.4 [func.wrap.func.inv]

R operator()(ArgTypes... args) const

效果: 调用 INVOKE(f, std::forward(args)..., R) (20.8.2),其中 f 是 *this 的目标对象(20.8.1)。

返回值: 如果 R 是 void,则返回 Nothing,否则返回 INVOKE (f, std::forward(args)..., R) 的返回值。

异常: 如果 !*this,则抛出 bad_function_call 异常;否则,抛出包装的可调用对象抛出的异常。

请注意,没有前置条件或要求条款。这意味着调用已被移动的 function 的调用运算符并不是未定义行为。无论 function 处于什么状态,您都不会违反任何先决条件。

请注意,规范从未说过调用将没有任何效果。因此,没有产生效果不是一种可能性。

调用将调用封装的函数或抛出 bad_function_call 异常。这是唯一的两个选择。它的行为取决于 function 对象的状态。而 function 对象的状态是未指定的 ([lib.types.movedfrom])。


在C++11中,如果您在资源管理器中使用std::function并在析构函数中调用它,如果该函数为空,则会触发std::terminate(),因为在析构函数中抛出异常会导致这种情况。 因此,情况稍有改变:先抛出 std::bad_function_call,然后是std::terminate() - user5154412
@villageidiot:改变从哪里开始?std::function与移动语义和析构函数上的noexcept一起成为标准(C++11)。 - Howard Hinnant
1
抱歉,措辞不当。我的意思是,如果在析构函数中进行“简单”的抛出操作,那么它将变成对std::terminate()的调用。这样说对吗? - user5154412
1
是的,除非你捕获它,或者除非你标记了析构函数为 noexcept(false)。而在后一种情况下,你必须非常小心,否则可能仍然会导致 std::terminate() - Howard Hinnant

23

根据20.8.11.2.1p6,function(function &&f)会使得f的值未指定但保持有效

空状态是一个有效的状态,因此您应该预期移动后的函数对象可以为空。

由于function执行类型抹除,并且函数对象可能非常昂贵,因此将移动后的对象保留为空的优化是有意义的:

std::function<void()> g{std::bind{f, std::array<int, 1000>{}}};
std::function<void()> h{std::move{g}};

在将 g 移动构造为 h 后,人们期望包含的 bind 已从 g 转移到了 h,而不是被复制,因此 g 会变为空。

对于下面的程序,gcc 4.5.1 打印出empty

#include <functional>
#include <iostream>
void f() {}
int main() {
    std::function<void()> g{f}, h{std::move(g)};
    std::cout << (g ? "not empty\n" : "empty\n");
}

这并不一定是最优的行为;内联小的可调用对象(例如函数指针)会导致复制可调用对象比移动并将原始对象清空更高效,因此另一种实现可能会使g保留其非空的可调用状态。


这是否意味着移动的函数对象可以被调用而不产生任何影响? - Martin
1
@Martin:调用它的效果是未指定或未定义的,我不太确定是哪一个(即不能调用它)。函数对象的一个有效状态的例子是它包装了一个空函数指针。调用它将具有未定义的行为。 - Steve Jessop
1
@Martin 不行。如果 f 被移动后为空,那么如果你调用它,它会抛出 bad_function_call 异常。 - Arne Mertz
@SteveJessop:std::function::operator bool() 至少会返回false吗? - Martin
5
@Martin:“未指定”意味着未指定。你不知道,所以不能假设它会发生。 - Nicol Bolas
显示剩余6条评论

9
移动的函数对象会保持有效状态,因此可以继续使用,但是它实际上处于未指定的状态。这意味着调用任何需要对象处于特定状态的函数将不能正常工作。
无法保证它为空,以便再次调用不产生影响。调用该函数要求它确实有一个待调用的函数,这是其状态的一部分。由于状态未指定,因此调用它的结果也是未指定的。
如果您想再次以某种有意义的方式使用该对象,请简单地创建一个新的“function”,并将其分配给它:
function<...> old;
function<...> new_ = std::move(old);
old = function<...>(...); //Reset to known state.
old(...); //Call is well-defined.

1

[func.wrap.func.con]:

function(function&& f);
template <class A> function(allocator_arg_t, const A& a, function&& f);

作用:如果 !f,则 * this 没有目标;否则,将 f 的目标移动构造到 * this 的目标中,使 f 保持有效状态但值未指定。

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