Lambda拾遗

Lambda的状态

一般来说, 我们会说当需要保持状态时, 我们会使用函数对象来实现, 这样写很直观. 但是同样可以使用lambda实现某些特别的效果. 例如下面的代码:

1
2
3
4
5
6
7
8
9
10
void NormalTester::test_case2()
{
int x = 1;
auto fun1 = [x]()mutable { TRACE() << "执行结果: " << ++x; };
TRACE() << "执行前: " << TSHOW(x);
fun1();
fun1();
fun1();
TRACE() << "执行后: " << TSHOW(x);
}

注意, 这里必须要在fun1定义为mutable, 否则编译会不过. 输出为:

1
2
3
4
5
QDEBUG : NormalTester::test_case2() [ NormalTester::test_case2 (...)] fun1执行前:  x:= 1 , 
QDEBUG : NormalTester::test_case2() [ NormalTester::test_case2::<lambda_1>::operator () (...)] fun1执行结果: 2
QDEBUG : NormalTester::test_case2() [ NormalTester::test_case2::<lambda_1>::operator () (...)] fun1执行结果: 3
QDEBUG : NormalTester::test_case2() [ NormalTester::test_case2::<lambda_1>::operator () (...)] fun1执行结果: 4
QDEBUG : NormalTester::test_case2() [ NormalTester::test_case2 (...)] fun1执行后: x:= 1 ,

可以看到, 每次调用fun1的时候x都被加1了, 但是外部的x并没有被修改. 说明lambda修改了x的一个副本, 并且在每次调用间保持了其状态.

道理还是很简单地. 它大概会被转换成下面的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int x = 1;
struct fun1class{
fun1class(int x) : _x{x}{}
void operator () mutable
{
TRACE() << "执行结果: " << ++_x;
}
}fun1(x);

fun1();
fun1();
fun1();
...

这里有两点:

  • 默认情况下, lambda是被const所修饰的, 所以值捕获的变量不能被修改–因为它被实现为函数对象的属性了. 如果我们希望能修改, 就要显式将其设置为mutable的.
  • 同一个lambda的实例的每次被调用, 它们的状态是可以被保存的.

于是, 如果我们这样写, 会是什么样子呢?

1
2
3
4
5
6
7
8
...
x = 1;
TRACE() << "fun3执行前: " << TSHOW(x);
for(int i=0; i<3; ++i)
{
[x]() mutable{TRACE() << "fun1执行结果: " << ++x;}();
}
TRACE() << "fun3执行后: " << TSHOW(x);

是的, 是这样:

1
2
3
4
5
QDEBUG : NormalTester::test_case2() [ NormalTester::test_case2 (...)] fun3执行前:  x:= 1 , 
QDEBUG : NormalTester::test_case2() [ NormalTester::test_case2::<lambda_3>::operator () (...)] fun1执行结果: 2
QDEBUG : NormalTester::test_case2() [ NormalTester::test_case2::<lambda_3>::operator () (...)] fun1执行结果: 2
QDEBUG : NormalTester::test_case2() [ NormalTester::test_case2::<lambda_3>::operator () (...)] fun1执行结果: 2
QDEBUG : NormalTester::test_case2() [ NormalTester::test_case2 (...)] fun3执行后: x:= 1 ,

这样做每次都是新的实例, 和上一次隔离了.

当然, 如果我们用引用捕获, 则是另一种情况, 这种太常见, 就不写了.

Lambda类型

关于Lambda, 有两点比较有意思的:

  • 每个定义的lambda都有自己独一无二的类型. 即使两个lambda的参数, 返回值, 甚至实现完全相同, 其类别也是不一样的.
  • Lambda可以保存到std::function中, 只要两个std::function的参数和返回值类型相同, 不管它们是否捕获参数, 都认为是相同的.

比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
void NormalTester::test_case3()
{
int val = 10;
auto fun1 = [](int x){ qDebug() << x ;};
auto fun2 = [](int x){ qDebug() << x ; };
std::function<void(int)> fun10 = [](int x){qDebug() << x ;};
std::function<void(int)> fun11 = [v=val](int x){ qDebug() << ++x << v; };

qDebug() << "\n" << TSHOW((std::is_same_v<decltype(fun1), decltype(fun2)>))
<< "\n" << TSHOW((std::is_same_v<decltype(fun10), decltype(fun11)>))
<< Qt::endl;
}

其输出是:

1
2
(std::is_same_v<decltype(fun1), decltype(fun2)>):= false ,  
(std::is_same_v<decltype(fun10), decltype(fun11)>):= true ,

在上面的例子中, 定义完全相同的fun1fun2是不同的类型, 而差别更大的func10func11却是相同类型的.
这样, std::function的一个很容易想到的用途就是作为指代lambda的参数类型. 但是, 使用std::function会有一定的性能损失. 这种场景下, 如果没有别的限制, 通常使用泛型将是更高效更普适的做法.

另外, 我们还可以直接将没有捕获块的lambda当作函数指针传递给以C方式声明的函数指针形参. 例如:

1
2
3
4
5
6
7
8
9
10
11
extern "C" 
void loginfo(const QString& msg, void(*callback)(const QString&))
{
callback(msg);
}
void NormalTester::test_case4()
{
auto logfunc = [](const QString& msg){ qDebug() << msg; };

loginfo("Hellow", logfunc);
}

我还在别处见过另一个新奇的说法, 说是这种情况下, 需要在lamba定义的前面显式加一个+才行, 不过我当前使用的编译器(MSVC2022, C++23标准)并不需要. 有可能是老版本C++编译器或标准的限制. 如果你的代码编译失败, 可以尝试修改一下试试:

1
auto logfunc = +[](const QString& msg){ qDebug() << msg; };

lambda的泛型

在C++17中引入了lambda的泛型. 当然其语法和传统的泛型函数有点差别. 它使用的是auto. 这个没啥特别的, 去看一下C++17的规范就可以了.

然后, 在C++20中, 又对lambda的泛型做了增强, 也支持传统函数的泛型写法了. 引入这种写法, 会给元编程带来不少便利.

1
2
3
4
5
6
7
8
void NormalTester::test_case5()
{
auto func1 = [](auto v){ return ++v;};
auto func2 = []<typename T>(T v){ return ++v; };

qDebug() << TSHOW(func1(12));
qDebug() << TSHOW(func2(12));
}

上面的函数, 只有指定c++20c++latest才能编译通过. 如果我们强制使用C++17, MSVC编译器对func2这行会这样报错:

1
2
D:\IveiLib\QtInPractice\source\ScanerSuit\UnitTest\UnitTest_NormalTester\tst_normaltester.cpp:135: error: C7563: 要使用模板参数列表创建 lambda,需要至少“/std:c++20”
D:\IveiLib\QtInPractice\source\ScanerSuit\UnitTest\UnitTest_NormalTester\tst_normaltester.cpp:135: warning: Explicit template parameter list for lambdas is a C++20 extension