【问题标题】:Move semantic with std::function使用 std::function 移动语义
【发布时间】:2026-02-11 20:15:01
【问题描述】:

std::function 提供来自右值 ref 的构造函数。 按照标准,移动的函数对象会发生什么?会不会是空的,所以再次调用它没有效果?

【问题讨论】:

  • 这取决于类的移动复制构造函数和/或移动赋值运算符。
  • @juanchopanza 你什么意思?当然,这取决于移动构造函数。它对原始函数对象有什么作用?搬家后会是空的吗?
  • 抱歉,我以为您的意思是通过绑定对象传递给可变参数构造函数的参数。

标签: c++ c++11 standards


【解决方案1】:

围绕这个问题有太多的困惑。我会尽量把事情说清楚...

本节描述标准定义对象的移出状态:

17.6.5.15 [lib.types.movedfrom]

C++ 标准库中定义的类型的对象可以从 (12.8)。移动操作可以显式指定或隐式指定 生成。除非另有规定,此类搬离对象应 置于有效但未指定的状态。

这是什么意思?这意味着给定一个标准定义的移动对象,您可以对该对象执行任何操作,而无需事先了解该对象的状态。不需要先验知识当前状态的一类动作是那些没有先决条件的动作。

例如,您可以在移出的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)。

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

抛出:bad_function_call if !*this;否则,抛出任何异常 通过包装的可调用对象。

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

请注意,在任何情况下,规范都没有说调用无效。所以没有效果是不可能的。

调用将调用包装函数,或抛出bad_function_call。这是仅有的两个选择。它的行为取决于function 对象的状态。并且function 对象的状态未指定([lib.types.movedfrom])。

【讨论】:

  • 在 C++11 中,如果你在资源管理器中使用 std::function 并在析构函数中调用它,如果函数为空,你将得到 std::terminate()ed,因为这就是你得到的用于抛出一个析构函数。所以这是一个小小的变化:std::bad_function_call throw 后跟 std::terminate()
  • @villageidiot:从什么改变? std::function 与移动语义和析构函数 (C++11) 上的 noexcept 同时成为标准。
  • 抱歉,措辞不好。我的意思是,如果在析构函数中完成,“仅仅”抛出就会成为对std::terminate() 的调用。对吗?
  • 是的,除非你抓住它,或者除非你标记析构函数noexcept(false)。在后一种情况下,您必须非常小心,否则可能会导致std::terminate()
【解决方案2】:

在 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:调用它的效果是未指定或未定义的,我不太确定是哪个(即不,你不能调用它)。函数对象的有效状态的一个示例是它包装了一个空函数指针。调用它会有未定义的行为。
  • @Martin:简单来说,函数对象有效到可以存在,但无效到可以使用。
  • @Martin 没有。如果 f 在被移动后为空,这意味着如果你调用它,它会抛出 bad_function_call
  • @Martin:“未指定”表示未指定。你不知道,所以你不能假设它会。
【解决方案3】:

按标准移动的函数对象会发生什么?

它将处于有效状态(因此可以使用该对象),但未指定它所处的实际状态。最后一部分意味着调用任何要求对象处于特定状态的函数不一定有效。

会不会是空的,所以再次调用它没有效果?

不能假设它会。调用该函数需要它实际上有一个要调用的函数。这是其状态的一部分。而且由于状态是未指定的,调用它的结果是未指定的。

如果您想再次以某种有意义的方式使用该对象,只需创建一个新的function 并将其分配给它:

function<...> old;
function<...> new_ = std::move(old);
old = function<...>(...); //Reset to known state.
old(...); //Call is well-defined.

【讨论】:

    【解决方案4】:

    [func.wrap.func.con]:

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

    效果:如果 !f,*this 没有目标;否则,将 f 的目标移动构造成 *this 的目标,使 f 处于有效状态,具有未指定的值。

    【讨论】: