【问题标题】:moving std::function into another std::function does not invoke move constructor on captured variables将 std::function 移动到另一个 std::function 不会在捕获的变量上调用移动构造函数
【发布时间】:2015-01-17 06:11:08
【问题描述】:

我有一个类 A 在构造/复制/移动时打印出一条消息

class A
{
public:
    A(std::string s)
        :s_(s)
    {
        std::cout << "A constructed\n";
    }
    ~A()
    {
        std::cout << "A destructed\n";
    }
    A(const A& a)
        :s_(a.s_)
    {
        std::cout << "A copy constructed\n";
    }
    A(A&& a)
       :s_(std::move(a.s_))
    {
        std::cout << "A moved\n";
    }
    A& operator=(const A& a)
    {
        s_ = a.s_;
        std::cout << "A copy assigned\n";
    }
    A& operator=(A&& a)
    {
        s_ = std::move(a.s_);
        std::cout << "A move assigned\n";
    }

    std::string s_;
};

main 中,我构造了一个A 的实例,在lambda 中按值捕获它,将该lambda 复制到std::function,最后将std::function 移动 到另一个std::function

int main()
{
    A a("hello ");
    std::function<void()> f = [a]{ std::cout << a.s_; };
    std::function<void()> g(std::move(f));
}

这会打印出以下内容

A constructed
A copy constructed
A copy constructed
A destructed
A destructed
A destructed

为什么A的移动构造函数没有被调用?将f 移动到g 的最后一步不应该调用A 的移动构造函数吗?

【问题讨论】:

  • coliru 移动它:coliru.stacked-crooked.com/a/9815dc53d6fe8a7e。我怀疑标准是否明确规定了这里应该发生什么,显然微软选择复制而不是移动。
  • 它甚至没有调用复制构造函数。构造g的行为没有调用A的任何构造函数
  • 你是对的;我一直假设第二个“构造的副本”来自 g 的构造,但实际上第一个来自 lambda 捕获,第二个来自构造 lambda 之外的 function
  • 仅仅改变一个对象的所有权不需要移动它。
  • GCC outputMSVC output 始终确定哪些行导致了哪些输出,副本不会出现在您认为的位置。

标签: c++ c++11 visual-studio-2013 lambda


【解决方案1】:

复制构造函数没有被精确地调用,因为你已经移动了std::function。这是因为std::function 可以选择将捕获的值存储在堆上并保留指向它们的指针。因此移动函数只需要移动那个内部指针。显然,MSVC 选择将捕获存储在堆上,而 GCC 等选择将它们存储在堆栈上,因此也需要移动捕获的值。

编辑:感谢 Mooing Duck 在 comment on the question 中指出 GCC 还将捕获存储在堆上。实际的区别似乎是 GCC 在从 lambda 构造时将捕获从 lambda 移动到 std::function

【讨论】:

  • 看起来真正的区别在于 gcc/clang 的 function(lambda&amp;&amp;) 版本移动了 lambda 捕获,但微软没有。
  • @dlf 我无法在此处粘贴 MSVC 标准库代码,但如果您在调试器中逐步执行此示例,您将看到行 std::function&lt;void()&gt; g(std::move(f)); 调用 function(_Myt&amp;&amp; _Right) 其中 std::forwards @ 987654329@ 转换为 _Resetm(_Myt&amp;&amp; _Right),它只是将 _Impl 指针与 _Right 交换并将 _Right 设置为 null。
  • 您还会注意到,_Resetm 包含检查移动的值是否存储在本地捕获,如果是,则移动捕获。
  • 对;但在此之前,当f 是从过期的 lambda 构造时,gcc 会移动捕获,而 vc++ 不会(实际的 MS 代码中有太多的预处理器,我不能轻易说出原因)。问题是关于某事;只是不像最初的样子。
  • @dlf 是的,我明白你现在在说什么了。抱歉,我之前专注于错误的路线。
【解决方案2】:

在这种情况下,您的标准库实现不使用小缓冲区优化, 因此,您的函数f 拥有一个指向堆分配 内存区域的指针,其中存储了a 的副本。由于您将f 移动到g,因此没有理由执行深度复制,并且实现只需将存储在f 中的函数的所有权移动到g(如unique_ptr)。

至于这里没有使用小缓冲区的原因,这可能与你的实现将functionmove构造函数定义为noexcept有关.

如果function的移动构造函数是noexcept,它不能调用任何可能抛出的函数,因此实现只是拒绝移动你的对象(从f的小缓冲区到g的一)并在堆上分配它,这样它就可以在移动构造函数/赋值中移动一个指针。

如果您只是将noexcept 添加到A 的复制构造函数,那么libstd++libc++ 都会在g = move(f) 行生成一个复制构造函数 调用。令人惊讶的是,他们似乎都忽略了 noexcept 移动构造函数的存在。


†请注意(至少在最新草案中)标准要求 function(function&amp;&amp;)non noexcept,但 libstd++ 和 libc++ 都将其实现为 noexcept,我无法检查目前退出 MSVC。

【讨论】:

    【解决方案3】:

    这似乎是 MSVC 的 std::function 移动构造函数的一个弱点。我在 Clang 3.3 上尝试了您的代码,它调用了 A 的移动构造函数。

    【讨论】:

    • std::function 没有在 clang、gcc 或 MSVC 中调用移动构造函数。 OP 误导了你。
    猜你喜欢
    • 1970-01-01
    • 2020-10-16
    • 2015-11-24
    • 1970-01-01
    • 1970-01-01
    • 2014-10-09
    • 2017-04-17
    • 1970-01-01
    相关资源
    最近更新 更多