如何使c++ lambda对象的存储更高效?

How can I make the storage of C++ lambda objects more efficient?

本文关键字:存储 高效 对象 何使 c++ lambda      更新时间:2023-10-16

我最近一直在考虑存储c++ lambda。您在互联网上看到的标准建议是将lambda存储在std::函数对象中。然而,这些建议都没有考虑到存储的影响。我突然想到,这背后一定有什么重要的巫术在起作用。考虑以下存储整数值的类:

class Simple {
public:
    Simple( int value ) { puts( "Constructing simple!" ); this->value = value; }
    Simple( const Simple& rhs ) { puts( "Copying simple!" ); this->value = rhs.value; }
    Simple( Simple&& rhs ) { puts( "Moving simple!" ); this->value = rhs.value; }
    ~Simple() { puts( "Destroying simple!" ); }
    int Get() const { return this->value; }
private:
    int value;
};
现在,考虑这个简单的程序:
int main()
{
    Simple test( 5 );
    std::function<int ()> f =
        [test] ()
        {
            return test.Get();
        };
    printf( "%dn", f() );
}

这是我希望从这个程序中看到的输出:

Constructing simple!
Copying simple!
Moving simple!
Destroying simple!
5
Destroying simple!
Destroying simple!

首先,我们创建值测试。我们在堆栈上为临时lambda对象创建一个本地副本。然后将临时lambda对象移动到由std::函数分配的内存中。我们销毁临时的。我们打印输出。我们销毁std::函数。最后,销毁测试对象。

不用说,这是不是我看到的。当我在Visual c++ 2010(发布或调试模式)上编译时,我得到了这样的输出:

Constructing simple!
Copying simple!
Copying simple!
Copying simple!
Copying simple!
Destroying simple!
Destroying simple!
Destroying simple!
5
Destroying simple!
Destroying simple!

天哪,效率太低了!编译器不仅没有使用我的move构造函数,而且在赋值过程中生成并销毁了两个明显多余的lambda副本。

所以,这里终于有了问题:(1)所有这些复制真的有必要吗?(2)是否有某种方法可以强制编译器生成更好的代码?感谢阅读!

您在网上看到的标准建议是将lambda存储在std::function对象中。然而,这些建议都没有考虑到存储的含义。

那是因为它不重要。您无法访问lambda的类型名。所以当你可以用auto存储它的原生类型时,它不会用那个类型离开那个作用域。你不能以那种类型返回。你可以只能把它插到别的地方。c++ 11提供的唯一的"其他东西"是std::function

所以你有一个选择:暂时持有它与auto,锁定在该范围内。或者把它放在std::function中长期保存。

这些复制真的有必要吗?

技术?不,对于std::function所做的,它不是必需的

是否有办法强制编译器生成更好的代码?

。这不是编译器的错;这就是std::function的具体实现方式。它可以做更少的复制;它不应该复制超过两次(并且取决于编译器如何生成lambda,可能只有一次)。

我注意到MSVC10的同样的性能问题,并在microsoftconnect上提交了一个bug报告:
https://connect.microsoft.com/VisualStudio/feedback/details/649268/std-bind-and-std-function-generate-a-crazy-number-of-copy

详细信息

该错误被关闭为"fixed"。与MSVC11开发人员预览您的代码,现在确实打印:

Constructing simple!
Copying simple!
Moving simple!
Destroying simple!
5
Destroying simple!
Destroying simple!

您的第一个问题很简单,即MSVC对std::function的实现效率低下。在g++ 4.5.1中,我得到:

Constructing simple!
Copying simple!
Moving simple!
Destroying simple!
5
Destroying simple!
Destroying simple!

这仍然会创建一个额外的副本。问题是,您的lambda按值捕获test,这就是您拥有所有副本的原因。试一试:

int main()
{
    Simple test( 5 );
    std::function<int ()> f =
        [&test] ()               // <-- Note added &
        {
            return test.Get();
        };
    printf( "%dn", f() );
}

再次使用g++,我现在得到:

Constructing simple!
5
Destroying simple!

请注意,如果你通过引用捕获,那么你必须确保testf的生命周期内保持存活,否则你将使用一个被销毁对象的引用,这将引发未定义的行为。如果f需要比test更长寿,那么你必须使用按值传递的版本。

使用c++ 14,您可以完全避免复制:

int main(){
    Simple test( 5 );
    std::function<int ()> f =[test = std::move(test)](){
        return test.Get();
    };
    printf( "%dn", f() );
}

输出:

Constructing simple!
Moving simple!
Moving simple!
Destroying simple!
5
Destroying simple!
Destroying simple!

注意下面这行:

[test = std::move(test)] 

这里,第一次出现"test"

问题是std::函数不使用move语义,并且在初始化时复制lambda。这是ms的一个糟糕的实现

这里有一个小技巧可以用来解决这个问题。

template<typename T>
class move_lambda
{
    T func_;
public:
    move_lambda(T&& func) : func_(std::move(func)){}            
    move_lambda(const move_lambda& other) : func_(std::move(other.func_)){} // move on copy 
    auto operator()() -> decltype(static_cast<T>(0)()){return func_();}
};
template <typename T>
move_lambda<T> make_move_lambda(T&& func)
{
    return move_lambda<T>(std::move(func));
}

用法:

int main()
{
    Simple test( 5 );
    std::function<int ()> f(make_move_lambda(
        [test] ()
        {
            return test.Get();
        }));
    printf( "%dn", f() );
}