将std :: function移动到另一个std :: function不会在捕获的变量上调用move构造函数

问题描述 投票:12回答:3

我有一个类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的移动构造函数吗?

c++ c++11 visual-studio-2013 lambda
3个回答
6
投票

由于您移动了std::function,因此未正确调用复制构造函数。这是因为std::function可以选择将捕获的值存储在堆上并保留指向它们的指针。因此,移动该函数只需要移动该内部指针。显然,MSVC选择将捕获存储在堆和GCC等上。选择将它们存储在堆栈上,因此也需要移动捕获的值。

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


3
投票

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

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

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

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


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


1
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.