放入lambda主体中的代码会被“转换”为对应闭包类型的()运算符中的代码。
默认情况下;在c;;11中;它是一个const内联成员函数。例如:
auto lam = [](double param) { /* do something*/ };
扩展为:
struct __anonymousLambda
{
inline void operator()(double param) const
{ /* do something */
}
};
当定义lambda时;无法创建接受不同参数的“重载”lambda。如:
// doesn;t compile!
auto lam = [](double param) { /* do something*/ };
auto lam = [](int param) { /* do something*/ };
test.cpp: In function ;int main();:
test.cpp:6:10: error: conflicting declaration ;auto lam;
6 | auto lam = [](int param) { /* do something*/ };
| ^~~
test.cpp:5:10: note: previous declaration as ;main()::<lambda(double)> lam;
5 | auto lam = [](double param) { /* do something*/ };
[]不仅引入了lambda表达式;还保存了一个捕获变量的列表。它被称为“捕获子句”。通过从lambda表达式外部捕获变量;可以在闭包类型中创建成员变量(非静态数据成员)。然后;在lambda表达式的主体中;可以访问它。
在c;; 98/03中对PrintFunctor做了类似的处理。在这个类中;添加了一个成员变量std::string strText;它是在构造函数中初始化的。成员变量允许在callable对象中存储状态
捕获的语法:
[&] 通过引用捕获在可到达作用域中声明的所有自动存储期变量。
[=] 通过值捕获(创建副本)在到达作用域中声明的所有自动存储期变量。
[x; &y]:显式地通过值捕获x;通过引用捕获y。
(args ……)通过值捕获模板参数包。
(&args ……)通过引用捕获模板参数包。
int x = 2 , y = 3 ;
const auto l1 = []() { return 1 ; }; // No capture
const auto l2 = [=]() { return x; }; // All by value (copy)
const auto l3 = [&]() { return y; }; // All by ref
const auto l4 = [x]() { return x; }; // Only x by value (copy)
// const auto lx = [=x]() { return x; }; // wrong syntax, no need for
// = to copy x explicitly
const auto l5 = [&y]() { return y; }; // Only y by ref
const auto l6 = [x, &y]() { return x * y; }; // x by value and y by ref
const auto l7 = [=, &x]() { return x ; y; }; // All by value except x
// which is by ref
const auto l8 = [&, y]() { return x - y; }; // All by ref except y which
// is by value
std::string str{;Hello World;};
auto foo = [str](){ std::cout << str << ;
;; };
foo();
对于上面的lambda表达式;str被value捕获(即它被复制)。编译器可能会生成如下的局部函数:
struct _unnamedLambda
{
_unnamedLambda(std::string s) : str(s) {} // copy
void operator() const
{
std::cout << str << ;
;;
}
std::string str;
};
将一个变量传递给构造函数;这在概念上称为lambda声明的“in-place”。
在上面展示的一个可能的构造函数(_unnamedLambda)仅用于演示目的;因为编译器可能以不同的方式实现它;并且不会公开它。
int x = 1, y = 1;
std::cout << x << ; ; << y << std::endl;
const auto foo = [&x, &y]() noexcept{ ;; x; ;; y; };
foo();
std::cout << x << ; ; << y << std::endl
对于上面的lambda表达式;编译器可能会生成如下的局部函子:
struct _unnamedLambda
{
_unnamedLambda(int &a, int &b) : x(a), y(b) {}
void operator() const noexcept
{
;;x;
;;y;
}
int &x;
int &y;
};
由于通过引用捕获x和y;因此闭包类型将包含同样是引用的成员变量。
虽然指定[=]或[&]可能很方便;因为它会捕获所有自动存储期变量;但显式地捕获变量更清晰。这样编译器就可以警告不必要的影响(参见关于全局和静态变量的说明)
The mutable Keyword
默认情况下;闭包类型的()运算符被标记为const;不能在lambda表达式的内部修改捕获的变量。如果想改变这种行为;需要在参数列表后面添加mutable关键字。这种语法实际上从闭包类型的调用操作符声明中删除了const。如果有一个简单的lambda表达式和一个可变的:
int x = 1 ;
auto foo = [x]() mutable { ;; x; };
它将被“扩展”为以下functor:
struct __lambda_x1
{
void operator()() { ;;x; }
int x;
};
可以看到;调用操作符可以更改成员字段的值。
#include <iostream>
int main()
{
const auto print = [](const char *str, int x, int y)
{
std::cout << str << ;: ; << x << ; ; << y << ;
;;
};
int x = 1, y = 1;
print(;in main();, x, y);
auto foo = [x, y, &print]() mutable
{
;;x;
;;y;
print(;in foo();, x, y);
};
foo();
print(;in main();, x, y);
}
in main(): 1 1
in foo(): 2 2
in main(): 1 1
在上面的例子中;可以改变x和y的值。因为它们只是所在作用域中x和y的副本;所以在foo被调用后;不会看到它们的新值。另一方面;如果是通过引用捕获的;那么就不需要对lambda应用mutable来修改值。这是因为捕获的成员变量是引用;不能在const成员函数中绑定;但可以更改引用的值。
int x = 1;
std::cout << x << ;
;;
const auto foo = [&x]() noexcept{ ;;x; };
foo();
std::cout << x << ;
;;
在上面的例子中;可以改变x和y的值。因为它们只是所在作用域中x和y的副本;所以在foo被调用后;不会看到它们的新值。另一方面;如果是通过引用捕获的;那么就不需要对lambda应用mutable来修改值。这是因为捕获的成员变量是引用;不能在const成员函数中绑定;但可以更改引用的值。
int x = 10 ;
const auto lam = [x]() mutable { ;; x; }
lam(); // doesn;t compile!
最后一行无法编译;因为无法在const对象上调用非const成员函数。
在继续讨论一些更复杂的捕获主题之前;可以稍微休息一下;专注于一个更实际的例子。当想使用标准库中的一些现有算法并改变默认行为时;Lambda表达式很方便。例如;对于std::sort;可以编写自己的比较函数。但是可以更进一步;使用调用计数器来增强comparator。看一看:
#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec{0, 5, 2, 9, 7, 6, 1, 3, 4, 8};
size_t compCounter = 0;
std::sort(vec.begin(), vec.end(),
[&compCounter](int a, int b) noexcept
{
;;compCounter;
return a < b;
});
std::cout << ;number of comparisons: ; << compCounter << ;
;;
for (const auto &v : vec)
std::cout << v << ;, ;;
return 0;
}
示例中提供的comparator与默认的comparator的工作方式相同;如果a小于b;它就返回;因此使用从最小到最大的自然顺序。不过;传递给std::sort的lambda表达式也会捕获局部变量compCounter。然后用这个变量来计算排序算法对这个比较器的调用次数。
number of comparisons: 36
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
如果有一个全局变量;并在lambda表达式中使用[=];可能会认为的全局对象也可以被value捕获……但事实并非如此。看代码:
#include <iostream>
int global = 10;
int main()
{
std::cout << global << std::endl;
auto foo = [=]() mutable noexcept{ ;;global; };
foo();
std::cout << global << std::endl;
const auto increaseGlobal = []() noexcept { ;;global; };
increaseGlobal();
std::cout << global << std::endl;
const auto moreIncreaseGlobal = [global]() noexcept { ;;global; };
moreIncreaseGlobal();
std::cout << global << std::endl;
return 0;
}
上面的例子定义了global;然后将它与main()函数中定义的几个lambda一起使用。如果运行代码;那么无论以何种方式捕获;它都将始终指向全局对象;并且不会创建本地副本。这是因为只有具有自动存储期的变量才能被捕获。GCC甚至会报告以下警告:
;capture of variable ;global; with non-automatic storage duration;
只有当显式地捕获全局变量时;这个警告才会出现;所以如果使用[=];编译器不会帮助。Clang编译器甚至更有用;因为它会生成一个错误:
error: ;global; cannot be captured because it does not have
automatic storage duration
与捕获全局变量类似;静态对象也会遇到同样的问题:
#include <iostream>
void bar()
{
static int static_int = 10;
std::cout << static_int << std::endl;
auto foo = [=]() mutable noexcept { ;;static_int; };
foo();
std::cout << static_int << std::endl;
const auto increase = []() noexcept { ;;static_int; };
increase();
std::cout << static_int << std::endl;
const auto moreIncrease = [static_int] () noexcept { ;;static_int; };
moreIncrease();
std::cout << static_int << std::endl;
}
int main()
{
bar();
}
这一次;试图捕获一个静态变量;然后改变它的值;但由于它没有自动存储期;编译器无法做到这一点。输出:
10
11
12
13
当捕获名为[static_int]的变量时;GCC会报告一个警告;而Clang会显示一个错误
在类成员函数中;如果想捕获成员变量;事情会变得更加复杂。由于所有数据成员都与this指针相关;因此它也必须存储在某个地方。
#include <iostream>
struct Baz
{
void foo()
{
const auto lam = [s](){ std::cout << s; };
lam();
}
std::string s;
};
int main()
{
Baz b;
b.foo();
}
代码试图捕获成员变量s。但是编译器会报错:
test.cpp: In member function ;void Baz::foo();:
test.cpp:6:27: error: capture of non-variable ;Baz::s;
6 | const auto lam = [s](){ std::cout << s; };
| ^
test.cpp:9:17: note: ;std::string Baz::s; declared here
9 | std::string s;
| ^
test.cpp: In lambda function:
test.cpp:6:46: error: ;this; was not captured for this lambda function
6 | const auto lam = [s](){ std::cout << s; };
| ^
test.cpp:6:46: error: invalid use of non-static data member ;Baz::s;
test.cpp:9:17: note: declared here
9 | std::string s;
要解决这个问题;必须捕获this指针。然后就可以访问成员变量。
#include <iostream>
struct Baz
{
void foo()
{
const auto lam = [this](){ std::cout << s <<std::endl; };
lam();
}
std::string s{;test;};
};
int main()
{
Baz b;
b.foo();
}
现在没有编译错误了。也可以使用[=]或[&]来捕获它(它们在c;; 11/14中具有相同的效果!)。请注意;通过值捕获到一个指针。这就是为什么可以访问成员变量;而不是它的副本。在c;;11中(甚至在c;; 14中);都不能这样写:
auto lam = [* this ]() { std:: cout << s; };;
代码无法在c;; 11/14中编译;但是;在c;;17中是允许的
如果在一个方法的上下文中使用lambda表达式;那么捕获它就很好。但是更复杂的情况呢?知道下面的代码会发生什么吗?
#include <functional>
#include <iostream>
struct Baz
{
std::function<void()> foo()
{
return [=]{ std::cout << s << std::endl; };
}
std::string s;
};
int main()
{
auto f1 = Baz{;abc;}.foo();
auto f2 = Baz{;xyz;}.foo();
f1();
f2();
}
这段代码声明了一个Baz对象;然后调用foo()。请注意;foo()返回的lambda(存储在std::function中)捕获了类的成员。4由于使用临时对象;不能确定当调用f1和f2时会发生什么。这是一个悬空引用问题;会产生未定义的行为。
struct Bar
{
std::string const &foo() const { return s; };
std::string s;
};
auto &&f1 = Bar{;abc;}.foo(); // a dangling reference
同样;如果显式地声明捕获([s]):
std::function<void()> foo()
{
return [s]{ std::cout << s << std::endl; };
}
总而言之;当lambda的寿命比对象本身长时;捕获这种情况可能会变得棘手。当使用异步调用或多线程时;可能会发生这种情况
如果有一个只能移动的对象(例如unique_ptr);那么就不能将它作为捕获变量移动到lambda。通过值进行捕获是行不通的;您只能通过引用捕获。
std::unique_ptr<int> p(new int{10});
auto foo = [p]() {}; // does not compile....
auto foo_ref = [&p]() {}; // compiles, but the ownership
// is not passed
在上面的例子中;可以看到捕获unique_ptr的唯一方法是通过引用。然而;这种方法可能不是最好的;因为它不转移指针的所有权。关于c;;14中;会看到这个问题被修复了;这要归功于捕获与初始化器。
如果捕获了一个常量变量;那么这个常量就会被保留:
#include <iostream>
#include <type_traits>
int main()
{
const int x = 10;
auto foo = [x]() mutable
{
std::cout << std::is_const<decltype(x)>::value << std::endl;
x = 11;
};
foo();
}
上面的代码无法编译;因为捕获的变量是常量。这个例子甚至尝试使用mutable;但这无济于事。
为了结束对capture子句的讨论;应该提到;还可以利用可变参数模板来捕获数据。编译器将包扩展为一个非静态数据成员的列表;如果想在模板代码中使用lambda;这可能很方便。例如;下面是一个实验捕获的代码示例:
#include <iostream>
#include <tuple>
template <class... Args>
void captureTest(Args... args)
{
const auto lambda = [args...]
{
const auto tup = std::make_tuple(args...);
std::cout << ;tuple size: ; << std::tuple_size<decltype(tup)>::value << ;
;;
std::cout << ;tuple 1st: ; << std::get<0>(tup) << ;
;;
};
lambda(); // call it
}
int main()
{
captureTest(1, 2, 3, 4);
captureTest(;Hello world;, 10.0f);
}
tuple size: 4
tuple 1st: 1
tuple size: 2
tuple 1st: Hello world
这段有点实验性的代码表明;可以通过值(也可以通过引用)捕获可变参数包;然后将包“存储”到元组对象中。然后;调用元组上的一些辅助函数来访问它的数据和属性。还可以使用c;;洞察来查看编译器如何生成代码并将模板、参数包和lambda表达式扩展为代码。请看这里的例子c;; Insight。
在很多情况下;可以跳过lambda的返回类型;然后编译器会替推断出typename。最初;返回类型推断仅限于主体中包含单个return语句的lambda表达式。然而;这个限制很快就被取消了;因为实现一个更方便的版本没有问题。从c;; 11开始;只要所有的return语句都是相同类型的;编译器就能够推断出返回类型。
#include <type_traits>
int main()
{
const auto baz = [](int x) noexcept
{
if (x < 20)
return x * 1.1;
else
return x * 2.1;
};
static_assert(std::is_same<double, decltype(baz(10))>::value,
;has to be the same!;);
}
在上面的lambda中;有两个return语句;但它们都指向double;因此编译器可以推断出类型。
如果想显式地指定返回类型;可以使用后面的返回类型规范。例如;当返回一个字符串字面量时:
#include <iostream>
#include <string>
int main()
{
const auto tesTSPeedString = [](int speed) noexcept
{
if (speed > 100)
return ;you;re a super fast;;
return ;you;re a regular;;
};
auto str = testSpeedString(100);
str ;= ; driver;; // uups! no ;= on const char*!
std::cout << str;
return 0;
}
上面的代码无法编译;因为编译器推断出lambda的返回类型是const char*。这是因为字符串字面量上没有;=运算符;因此代码中断。
可以通过显式地将返回类型设置为std::string来解决这个问题:
auto testSpeedString = [](int speed) -> std::string
{
if (speed > 100)
return ;you;re a super fast;;
return ;you;re a regular;;
};
auto str = testSpeedString(100);
str ;= ; driver;; // works fine
请注意;现在必须删除noexcept;因为创建std::string时可能会抛出异常。顺便提一下;还可以使用命名空间std::string_literals;然后返回“you’re a regular”表示std::string类型
为了说明lambda如何支持这种转换;让考虑下面的例子。它定义了一个显式定义转换操作符的函子baz:
#include <iostream>
void callWith10(void (*bar)(int))
{
bar(10);
}
int main()
{
struct
{
using f_ptr = void (*)(int);
void operator()(int s) const { return call(s); }
operator f_ptr() const { return &call; }
private:
static void call(int s) { std::cout << s << ;
;; };
} baz;
callWith10(baz);
callWith10([](int x){ std::cout << x << ;
;; });
}
在上述程序中;有一个函数callWith10接受一个函数指针作为参数。然后使用两个参数调用它(第18行和第19行):第一个使用baz;这是一个包含必要的转换操作符的functor -它转换为f_ptr;这与callWith10的输入参数相同。稍后;会调用lambda表达式。在这种情况下;编译器在底层执行所需的转换。
当需要调用需要回调的c风格函数时;这种转换可能很方便。例如;下面的代码调用了C库中的qsort;并使用lambda对元素进行逆序排序:
#include <cstdlib>
#include <iostream>
int main()
{
int values[] = {8, 9, 2, 5, 1, 4, 7, 3, 6};
constexpr size_t numElements = sizeof(values) / sizeof(values[0]);
std::qsort(values, numElements, sizeof(int),
[](const void *a, const void *b) noexcept
{
return (*(int *)b - *(int *)a);
});
for (const auto &val : values)
std::cout << val << ;, ;;
}
正如在代码示例中看到的那样;std::qsort只使用函数指针作为比较器。编译器可以对传入的无状态lambda表达式进行隐式转换。
在继续讨论另一个话题之前;还有一个案例值得分析:
#include <type_traits>
int main()
{
auto funcPtr = ;[] {};
static_assert(std::is_same<decltype(funcPtr), void (*)()>::value);
}
请注意;的奇怪语法。如果移除加号;那么static_assert会失败。这是为什么呢?
代码中使用了一元运算符;。这个运算符可以作用于指针;因此编译器会将无状态lambda转换为函数指针;然后将其赋值给funcPtr。另一方面;如果删除加号;那么funcPtr就只是一个普通的闭包对象;这就是static_assert失败的原因。虽然用“;”编写这样的语法可能不是最好的主意;但如果编写static_cast;它具有相同的效果。当不希望编译器创建太多函数实例时;可以使用这种技术。例如:
template <typename F>
void call_function(F f)
{
f(10);
}
int main()
{
call_function(static_cast<int (*)(int)>([](int x){ return x ; 2; }));
call_function(static_cast<int (*)(int)>([](int x){ return x * 2; }));
}
在上面的例子中;编译器只需创建call_function的单个实例——因为它只接受一个函数指针int (*)(int)。但是如果移除static_cast;那么会得到两个版本的call_function;因为编译器必须为lambda创建两个单独的类型
在大多数例子中;可以注意到我定义了一个lambda表达式;然后调用它。
不过;也可以立即调用lambda:
#include <iostream>
int main()
{
int x = 1, y = 1;
[&]() noexcept{ ;; x; ;; y; }(); // <-- call ()
std::cout << x << ;, ; << y;
}
正如在上面看到的;lambda被创建并且没有被赋值给任何闭包对象。但随后它被调用与()。如果运行这个程序;会看到输出为2,2。
当需要对const对象进行复杂的初始化时;这种表达式可能很有用。
const auto val = []()
{
/* several lines of code... */
}(); // call it!
上面的val是lambda表达式返回类型的常量;即:
// val1 is int
const auto val1 = []() { return 10 ; }();
// val2 is std::string
const auto val2 = []() -> std:: string { return ;ABC; ; }();
#include <iostream>
#include <string>
void ValidateHTML(const std::string &) {}
std::string BuildAHref(const std::string &link, const std::string &text)
{
const std::string html = [&link, &text]
{
const auto &inText = text.empty() ? link : text;
return ;<a href= ; ; ; link ; ; ; >; ; inText ; ;</a>;;
}(); // call!
ValidateHTML(html);
return html;
}
int main()
{
try
{
const auto ahref = BuildAHref(;www.leanpub.com;, ;Leanpub Store;);
std::cout << ahref;
}
catch (...)
{
std::cout << ;bad format...;;
}
}
上面的例子包含一个函数BuildAHref;它接受两个参数;然后构建一个 HTML标签。根据输入参数;构建html变量。如果文本不为空;则使用它作为内部HTML值。否则;使用链接。希望html变量是const;但在输入参数中添加必要的条件;很难编写紧凑的代码。多亏了IIFE;可以写一个单独的lambda表达式;然后用const标记变量。稍后可以将该变量传递给ValidateHTML。
可能会惊讶地发现;也可以从lambda派生!因为编译器使用operator()将lambda表达式扩展为一个functor对象;所以可以继承这个类型。
#include <iostream>
template <typename Callable>
class ComplexFunctor : public Callable
{
public:
explicit ComplexFunctor(Callable f) : Callable(f) {}
};
template <typename Callable>
ComplexFunctor<Callable> MakeComplexFunctor(Callable &&cal)
{
return ComplexFunctor<Callable>(cal);
}
int main()
{
const auto func = MakeComplexFunctor([](){ std::cout << ;Hello Functor!;; });
func();
}
在这个例子中;ComplexFunctor类派生自一个模板参数Callable。如果想从lambda表达式中派生;需要做一个小技巧;因为无法明确说出闭包类型的确切类型(除非把它包装在std::function中)。这就是为什么需要MakeComplexFunctor函数;它可以执行模板参数推断;并得到lambda闭包的类型。除了名字之外;ComplexFunctor只是一个简单的包装器;没有太多用途。这种代码模式有什么用例吗?例如;可以扩展上面的代码并继承两个lambda并创建一个重载的集合:
这一次有更多的代码:从两个模板参数派生;但还需要显式地暴露它们的调用操作符。这是为什么呢?这是因为在寻
找正确的函数重载时;编译器要求候选函数在相同的作用域内。为了理解这一点;让写一个派生于两个基类的简单类型。这个例子还注释了两个using语句:
#include <iostream>
struct BaseInt
{
void Func(int) { std::cout << ;BaseInt...
;; }
};
struct BaseDouble
{
void Func(double) { std::cout << ;BaseDouble...
;; }
};
struct Derived : public BaseInt, BaseDouble
{
// using BaseInt::Func;
// using BaseDouble::Func;
};
int main()
{
Derived d;
d.Func(10.0);
}
将lambda表达式存储在容器中
#include <iostream>
#include <vector>
int main()
{
using TFunc = void (*)(int &);
std::vector<TFunc> ptrFuncVec;
ptrFuncVec.push_back([](int &x){ std::cout << x << ;
;; });
ptrFuncVec.push_back([](int &x){ x *= 2; });
ptrFuncVec.push_back(ptrFuncVec[0]); // print it again;
int x = 10;
for (const auto &entry : ptrFuncVec)
entry(x);
return 0;
}
#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>
int main()
{
std::vector<std::function<std::string(const std::string &)>> vecFilters;
size_t removedSpaceCounter = 0;
const auto removeSpaces = [&removedSpaceCounter](const std::string &str)
{
std::string tmp;
std::copy_if(str.begin(), str.end(), std::back_inserter(tmp),
[](char ch){ return !isspace(ch); });
removedSpaceCounter ;= str.length() - tmp.length();
return tmp;
};
const auto makeUpperCase = [](const std::string &str)
{
std::string tmp = str;
std::transform(tmp.begin(), tmp.end(), tmp.begin(),
[](unsigned char c){ return std::toupper(c); });
return tmp;
};
vecFilters.emplace_back(removeSpaces);
vecFilters.emplace_back([](const std::string &x){ return x ; ; Amazing;; });
vecFilters.emplace_back([](const std::string &x){ return x ; ; Modern;; });
vecFilters.emplace_back([](const std::string &x){ return x ; ; C;;;; });
vecFilters.emplace_back([](const std::string &x){ return x ; ; World!;; });
vecFilters.emplace_back(makeUpperCase);
const std::string str = ; H e l l o ;;
auto temp = str;
for (const auto &entryFunc : vecFilters)
temp = entryFunc(temp);
std::cout << temp;
std::cout << ;
removed spaces: ; << removedSpaceCounter << ;
;;
}
这次在容器中存储了std::function<std::string(const std::string&)>。这允许使用任何类型的函数对象;包括带捕获变量的lambda表达式。其中一个lambda removeSpacesCnt捕获了一个变量;该变量用于存储输入字符串中被删除的空格的信息。
C;;14
在c;; 14中;可以在函数调用中使用默认参数。这是一个小特性;但使lambda更像一个常规函数
#include <iostream>
int main()
{
const auto lam = [](int x = 10){ std::cout << x << ;
;; };
lam();
lam(100);
}
如果有多个return语句;它们都必须推断出相同的类型:
auto foo = [](int x)
{
if (x < 0)
return x * 1.1f; // float!
else
return x * 2.1; // double!
};
上面的代码无法编译;因为第一个return语句返回float而第二个double。编译器无法决定;所以必须选择单一类型。虽然推断整数或双精度浮点数可能很有用;但返回类型推断的价值还有更重要的原因。此功能在模板代码和“未知”事物中发挥了相当大的作用。例如;lambda闭包类型是匿名的;不能在代码中显式指定它。如果想从函数返回一个lambda;那么如何指定类型?
#include <functional>
#include <iostream>
std::function<int(int)> CreateMulLambda(int x)
{
return [x](int param) noexcept{ return x * param; };
}
int main()
{
const auto lam = CreateMulLambda(10);
std::cout << sizeof(lam);
return lam(2);
}
然而;上述解决方案并不简单。它需要指定函数签名;甚至包括一些额外的头文件。回想一下在c;; 11章中;std::function是一个重量级对象(在GCC 9中;sizeof显示32字节);它需要一些高级的内部机制才能处理任何可调用对象。多亏了c;; 14的改进;现在可以大大简化代码:
#include <iostream>
auto CreateMulLambda(int x) noexcept
{
return [x](int param) noexcept{ return x * param; };
}
int main()
{
const auto lam = CreateMulLambda(10);
std::cout << sizeof(lam);
return lam(2);
}
这一次可以完全依赖编译时类型推断;不需要任何辅助类型。在GCC上;lambda sizeof(lam)的大小只有4字节;而且比使用std::function的解决方案要便宜得多。请注意;还可以将CreateMulLambda标记为noexcept;因为它不会抛出任何异常。在返回std::function时;情况并非如此。
现在有一些更重要的更新!应该还记得;在lambda表达式中;可以从外部作用域捕获变量。编译器扩展捕获语法并在闭包类型中创建成员变量(非静态数据成员)。现在;在c;;14中;可以创建新的成员变量并在capture子句中初始化它们。然后可以在lambda中访问这些变量。它被称为带有初始化器的捕获;或者这个功能的另一个名字是泛化的lambda捕获
#include <iostream>
int main()
{
int x = 30;
int y = 12;
const auto foo = [z = x ; y](){ std::cout << z << ;
;; };
x = 0;
y = 0;
foo();
}
42
在上面的例子中;编译器生成了一个新的成员变量;并用x;y初始化它。推断新变量的类型的方式与将auto放在这个变量前面的方式相同。在的例子中:
auto z = x ; y;
总而言之;上例中的lambda表达式会被解析为如下(简化后的)functor:
struct _unnamedLambda
{
void operator()() const
{
std::cout << z << ;
;;
}
int z;
} someInstance;
在定义lambda表达式时;Z会直接初始化(用x;y)。记住前面的句子。新变量是在定义lambda表达式的地方初始化的;而不是在调用它的地方。这就是为什么如果在创建lambda之后修改x或y变量;变量z不会改变。在这个例子中;可以看到;在定义lambda表达式之后;我立即改变了x和y的值。然而;由于z之前被初始化;输出仍然是42。
通过初始化器创建变量也很灵活;例如;可以从外部作用域创建对变量的引用。
#include <iostream>
int main()
{
int x = 30;
const auto foo = [&z = x](){ std::cout << z << ;
;; };
foo();
x = 0;
foo();
}
这一次;变量z是对x的引用。它的创建方式和写的一样:
auto & z = x;
如果运行这个例子;应该看到第一行打印了30;而第二行显示了0。这是因为捕获了一个引用;所以当修改引用的变量时;z对象也会改变。
请注意;虽然可以用initialiser作为引用来捕获;但不能写r值引用&&。这就是下面的代码无效的原因:
[&& z = x] // invalid syntax!
换句话说;在c;; 14中;不能这样写:
template <class... Args>
auto captureTest(Args... args)
{
return lambda = [... capturedArgs = std::move(args)]() {};
以前;在c;; 11中;无法通过值捕获唯一的指针。只能通过引用捕获。现在;从c;; 14开始;可以将对象移动到闭包类型的成员中
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> p(new int{10});
const auto bar = [ptr = std::move(p)]
{
std::cout << ;pointer in lambda: ; << ptr.get() << ;
;;
};
std::cout << ;pointer in main(): ; << p.get() << ;
;;
bar();
}
pointer in main(): 0
pointer in lambda: 0x1413c20
多亏了捕获初始化方法;可以将指针的所有权转移到lambda中。正如在例子中看到的;在创建闭包对象之后;唯一的指针被设置为nullptr。但当调用lambda时;会看到一个有效的内存地址
std:: unique_ptr< int > p(new int {10 });
std:: function< void ()> fn = [ptr = std:: move(p)]() { }; // won;t compile!
另一个想法是使用捕获初始化作为一种潜在的优化技术。与其每次调用lambda都计算某个值;不如在初始化器中计算一次:
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main()
{
using namespace std::string_literals;
const std::vector<std::string> vs = {;apple;, ;orange;,
;foobar;, ;lemon;};
const auto prefix = ;foo; s;
auto result = std::find_if(vs.begin(), vs.end(),
[&prefix](const std::string &s)
{
return s == prefix ; ;bar; s;
});
if (result != vs.end())
std::cout << prefix << ;-something found!
;;
result = std::find_if(vs.begin(), vs.end(),
[savedString = prefix ; ;bar; s](const std::string &s)
{
return s == savedString;
});
if (result != vs.end())
std::cout << prefix << ;-something found!
;;
}
上面的代码显示了对std::find_if的两次调用。在第一个场景中;捕获前缀并将输入值与前缀;“bar”进行比较。每次调用lambda时;都必须创建并计算一个临时值;其中存储了这些字符串的和。对find_if的第二次调用显示了一个优化:创建了一个捕获的变量savedString;用于计算字符串的总和。然后;就可以安全地在lambda主体中引用它了。字符串的和只会运行一次;而不是每次调用lambda时都运行。这个例子还使用了std::string_literals;这就是为什么可以编写表示std::string对象的“foo”
initializer也可以用来捕获成员变量。然后;可以捕获成员变量的副本;而不用担心悬空引用。
#include <algorithm>
#include <iostream>
struct Baz
{
auto foo() const
{
return [s = s]
{ std::cout << s << std::endl; };
}
std::string s;
};
int main()
{
const auto f1 = Baz{;abc;}.foo();
const auto f2 = Baz{;xyz;}.foo();
f1();
f2();
}
在foo()中;通过将成员变量复制到闭包类型中来捕获成员变量。此外;还使用auto来推断成员函数foo()的返回类型。顺便说一句;在c;; 11中;将不得不使用std::function;请参见c;; 11章节。在声明lambda表达式时使用了[s = s]这样的“奇怪”语法;这可能令人惊讶。这段代码之所以能工作;是因为捕获的变量在闭包类型的作用域内;而不是在外部作用域内。这就是为什么这里没有冲突
这可是个大问题!lambda的早期规范允许创建匿名函数对象;并将它们传递给标准库中的各种泛型算法。然而;闭包本身并不是“通用的”。例如;不能将模板参数指定为lambda参数。幸运的是;从c;; 14开始;标准引入了泛型lambda;现在可以这样写:
const auto foo = [](auto x, int y) { std:: cout << x << ;, ; << y << ;
; ; };
foo(10 , 1 );
foo(10.1234 , 2 );
foo(;hello world; , 3 );
请注意;autox是lambda表达式的参数。这相当于在闭包类型的call操作符中使用模板声明:
struct
{
template <typename T>
void operator()(T x, int y) const
{
std::cout << x << ;, ;<<y << ;
; ;
}
} someInstance;
如果有更多的自动参数;代码将展开为单独的模板参数:
const auto fooDouble = [](auto x, auto y) { /*...*/ };
扩展为:
struct
{
template <typename T, typename U>
void operator()(T x, U y) const
{ /*...*/
}
} someOtherInstance;
但这还不是全部。如果需要更多的函数参数;那么也可以使用“可变参数”
#include <iostream>
template <typename T>
auto sum(T x) { return x; }
template <typename T1, typename... T>
auto sum(T1 s, T... ts) { return s ; sum(ts...); }
int main()
{
const auto sumLambda = [](auto... args)
{
std::cout << ;sum of: ; << sizeof...(args) << ; numbers
;;
return sum(args...);
};
std::cout << sumLambda(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9);
}
在上面的示例中;通用lambda使用了auto…表示可变参数包。从概念上讲;它可以扩展为以下调用操作符:
struct __anonymousLambda
{
template <typename... T>
void operator()(T... args) const
{ /*...*/
}
};
使用通用lambda表达式时;不仅可以使用auto x;还可以像使用其他自动变量一样添加任何限定符;如auto&、const auto&或auto&&。其中一个方便的用例是;可以指定auto&& x;它成为一个转发(通用)引用。这允许完美地转发输入参数:
#include <iostream>
#include <string>
void foo(const std::string &) { std::cout << ;foo(const string&)
;; }
void foo(std::string &&) { std::cout << ;foo(string&&)
;; }
int main()
{
const auto callFoo = [](auto &&str)
{
std::cout << ;Calling foo() on: ; << str << ;
;;
foo(std::forward<decltype(str)>(str));
};
const std::string str = ;Hello World;;
callFoo(str);
callFoo(;Hello World Ref Ref;);
}
Calling foo() on: Hello World
foo(const string&)
Calling foo() on: Hello World Ref Ref
foo(string&&)
示例代码定义了两个函数重载foo;其中一个重载了对std::string的const引用;另一个重载了对std::string的r值引用。callFoo lambda使用了一个泛型参数;这是一个通用引用。如果想把这个lambda重写为一个普通的函数模板;它看起来像这样:
template <typename T>
void callFooFunc(T &&str)
{
std::cout << ;Calling foo() on: ; << str << ;
;;
foo(std::forward<T>(str));
}
正如在泛型lambda中看到的;有更多的选择来编写局部匿名函数。
此外;当类型推断比较棘手时;泛型lambda可能非常有用。
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
int main()
{
const std::map<std::string, int> numbers{{;one;, 1}, {;two;, 2}, {;three;, 3}};
// each time entry is copied from pair<const string, int>!
std::for_each(std::begin(numbers), std::end(numbers),
[](const std::pair<std::string, int> &entry)
{
std::cout << entry.first << ; = ; << entry.second << ;
;;
});
}
这是错误的;因为std::map的值类型是std::pair<const Key, T>;而不是const std::pair<Key, T>。在的例子中;由于在std::pair<const std::string, int>和const std::pair<std::string, int>&之间进行了转换;所以代码执行了额外的复制。将const std::string转换为std::string:
这个问题可以用auto来修复:
std::for_each(std::begin(numbers), std::end(numbers),
[](const auto &entry)
{
std::cout << entry.first << ; = ; << entry.second << ;
;;
});
现在模板参数演绎将充分获得条目对象的正确类型;并且不会创建额外的副本。更不用说代码更容易阅读和更短。请参阅完整的示例;其中还包含打印各数据项地址的代码:
Lambdas in C;;17
类型系统中的异常规范
在介绍lambda的语法改进之前;需要介绍c;; 17中引入的一个“通用”语言特性。函数的异常规范过去并不属于函数类型;但现在;在c;;17中;它是函数类型的一部分。这意味着可以有两个函数重载:一个有noexcept;另一个没有。见下文:
using TNoexceptVoidFunc = void (*)() noexcept;
void SimpleNoexceptCall(TNoexceptVoidFunc f) { f(); }
using TVoidFunc = void (*)();
void SimpleCall(TVoidFunc f) { f(); }
void fNoexcept() noexcept {}
void fRegular() {}
int main()
{
SimpleNoexceptCall(fNoexcept);
SimpleNoexceptCall([]() noexcept {});
// SimpleNoexceptCall(fRegular); // cannot convert
// SimpleNoexceptCall([]() { }); // cannot convert
SimpleCall(fNoexcept); // converts to regular function
SimpleCall(fRegular);
SimpleCall([]() noexcept {}); // converts
SimpleCall([]() {});
}
指向noexcept函数的指针可以转换为指向普通函数的指针(这也适用于指向成员函数的指针和lambda表达式)。但反过来是不可能的(从普通函数指针到标记为noexcept的函数指针)。添加该功能的原因之一是有机会更好地优化代码。如果编译器能保证函数不会抛出异常;那么它可能会生成更快的代码10。在标准库中;有很多地方会检查noexcept;从而提高代码的效率。这就是std::vector的工作原理;它可以区分存储类型是否可以在移动时不抛出异常。下面是一个使用type traits和if constexpr来检查给定的可调用对象是否标记为noexcept的例子:
#include <iostream>
#include <type_traits>
template <typename Callable>
void CallWith10(Callable &&fn)
{
if constexpr (std::is_nothrow_invocable_v<Callable, int>)
{
std::cout << ;Calling fn(10) with optimisation
;;
fn(10);
}
else
{
std::cout << ;Calling fn(10) normally
;;
fn(10);
}
}
int main()
{
int x{10};
const auto lam = [&x](int y) noexcept { x ;= y; };
CallWith10(lam);
const auto lamEx = [&x](int y)
{
std::cout << ;lamEx with x = ; << x << ;
;;
x ;= y;
};
CallWith10(lamEx);
}
Calling fn(10) with optimisation
Calling fn(10) normally
lamEx with x = 20
#include <array>
template <typename Range, typename Func, typename T>
constexpr T SimpleAccumulate(Range &&range, Func func, T init)
{
for (auto &&elem : range)
{
init ;= func(elem);
}
return init;
}
int main()
{
constexpr std::array arr{1, 2, 3};
constexpr auto sum = SimpleAccumulate(arr, [](auto i){ return i * i; }, 0);
static_assert(sum == 14);
}
这段代码使用了一个constexpr lambda表达式;它被传递给了SimpleAccumulate。lambda表达式没有显式地标记为constexpr;但编译器会用constexpr声明它的调用操作符;因为函数体中只包含一个简单的计算。该算法还使用了一些c;;17元素:constexpr对std::array、std::begin和std::end的添加现在也是constexpr;这意味着整个代码可能会在编译时执行。
在c;;14;讨论了可用于泛型lambda表达式的可变参数列表。多亏了c;; 17中的折叠表达式;可以编写更紧凑的代码。下面是转换后的求和计算示例:
#include <iostream>
int main()
{
const auto sumLambda = [](auto... args)
{
std::cout << ;sum of: ; << sizeof...(args) << ; numbers
;;
return (args ; ... ; 0);
};
std::cout << sumLambda(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9);
}
如果将它与前面c;;14中的示例进行比较;会很快注意到它不需要递归!折叠表达式为提供了一种简单且相对直观的语法;用于编写组合可变参数的表达式
#include <iostream>
int main()
{
const auto printer = [](auto... args)
{
(std::cout << ... << args) << ;
;;
};
printer(1, 2, 3, ;hello;, 10.5f);
}
123hello10.5
为了解决这个问题;可以引入一个辅助方法;将逗号运算符而不是<<折叠起来:
#include <iostream>
int main()
{
const auto printer = [](auto... args)
{
const auto printElem = [](auto elem)
{
std::cout << elem << ;, ;;
};
(printElem(args), ...);
std::cout << ;
;;
};
printer(1, 2, 3, ;hello;, 10.5f);
}
1, 2, 3, hello, 10.5,
const auto printer = [](auto... args)
{
((std::cout << args << ;, ;), ...);
std::cout << ;
;;
};
如果不想显示打印序列末尾的最后一个逗号;可以这样做:
#include <iostream>
int main()
{
const auto printer = [](auto first, auto... args)
{
std::cout << first;
((std::cout << ;, ; << args), ...);
std::cout << ;
;;
};
printer(1, 2, 3, ;hello;, 10.5f);
}
这一次;需要为第一个条目使用一个通用模板参数;然后为其余条目使用一个可变参数列表。然后;可以打印第一个元素;并在其他元素之前添加一个逗号。代码将打印:
1, 2, 3, hello, 10.5
在c;;11章中;学习了如何从lambda表达式派生。虽然看到这样的技术很有趣;但用例是有限的。这种方法的主要问题是它只支持特定数量的lambda。这些例子使用了一两个基类。但是;如果使用数量不定的基类;也就是使用数量不定的lambda呢?在c;;17中;有一个相对简单的模式!
template < class... Ts> struct overloaded : Ts... { using Ts:: operator ()...; };
template < class... Ts> overloaded(Ts...) -> overloaded< Ts...> ;
如所见;需要使用可变参数模板;因为它们允许指定基类的可变数量。下面是一个使用该代码的简单示例:
#include <iostream>
template <class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
int main()
{
const auto test = overloaded{
[](const int &i) { std::cout << ;int: ; << i << ;
;; },
[](const float &f) { std::cout << ;float: ; << f << ;
;; },
[](const std::string &s) { std::cout << ;string: ; << s << ;
;; }
};
test(;10.0f;);
}
string: 10.0f
在上面的例子中;创建了一个由三个lambda组成的测试对象。然后可以用一个参数调用这个对象;根据输入参数的类型选择正确的lambda。现在让仔细看看这个模式的核心部分。这两行代码得益于自c;;17以来提供的三个特性:
使用声明打包扩展——使用可变参数模板的简短而紧凑的语法。
自定义模板参数推演规则——允许将lambda对象的列表转换为重载类的基类列表。(注意:在c;;20中不需要!)
聚合初始化的扩展——在c;;17之前;不能聚合初始化派生自其他类型的类型。
在c;; 11中;已经介绍过使用声明的必要性。这对于将调用操作符带入重载结构的相同作用域非常重要。在c;; 17中;得到了一种支持可变参数模板的语法;这在该语言的之前版本中是不可能的。
从lambda表达式派生而来;然后像上一节中那样暴露它们的运算符()。但是如何创建这种重载类型的对象呢?如所知;无法预先知道lambda的类型;因为编译器必须为每个lambda生成一个唯一的类型名。例如;不能这样写:
overload< LambdaType1, LambdaType2> myOverload { ... } // ???
唯一可行的方法是一些make函数(因为模板参数推断适用于函数模板;因为总是):
template <typename... T>
constexpr auto make_overloader(T &&...t)
{
return overloaded<T...>{std::forward<T>(t)...};
}
使用在c;; 17中添加的模板参数推导规则;可以简化通用模板类型的创建;并且不需要make_overloader函数。例如;对于简单类型;可以这样写:
std:: pair strDouble { std:: string{;Hello; }, 10.0 };
// strDouble is std::pair<std::string, double>
还有一个选项可以定义自定义扣款指南。标准库中大量使用了它们;例如std::array:
template < class T , class... U>
array(T, U...) -> array< T, 1 ; sizeof ...(U)> ;
array test{1 , 2 , 3 , 4 , 5 };
// test is std::array<int, 5>
对于重载模式;可以指定一个自定义的推导指南:
template < class... Ts> overloaded(Ts...) -> overloaded< Ts...> ;
现在;当用两个lambda初始化重载时:
overloaded myOverload { [](int ) { }, [](double ) { } };
这个功能相对简单:现在可以聚合初始化一个派生自其他类型的类型。
struct base1
{
int b1, b2 = 32;
};
struct base2
{
base2() { b3 = 64; }
int b3;
};
struct derived : base1, base2
{
int d;
};
derived d1{{1, 2}, {}, 4};
derived d2{{}, {}, 4};
代码用 1 初始化 d1.b1;用 2 初始化 d1.b2;用 64 初始化 d1.b3;用 4 初始化 d1.d
.对于第二个对象;代码用 0 初始化 d2.b1;用 32 初始化 d2.b2;用 64 初始化 d2.b3;用 4 初始化 d2.d
在的例子中;它有更重要的影响。因为对于重载类来说;没有聚合初始化;必须实现以下构造函数:
struct overloaded : Fs...
{
template <class... Ts>
overloaded(Ts &&...ts) : Fs{std::forward<Ts>(ts)}...
{
}
// ...
}
有了这些知识;就可以使用继承和重载模式来做一些更实际的事情。看一个访问std::variant的例子:
#include <iostream>
#include <variant>
template <class... Ts>
struct overloaded : Ts...
{
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
int main()
{
const auto PrintVisitor = [](const auto &t)
{ std::cout << t << ;
;; };
std::variant<int, float, std::string> intFloatString{;Hello;};
std::visit(PrintVisitor, intFloatString);
std::visit(overloaded{[](int &i) { i *= 2; },
[](float &f) { f *= 2.0f; },
[](std::string &s) { s = s ; s; }
},intFloatString);
std::visit(PrintVisitor, intFloatString);
}
Lambdas with std::thread
让从c;;11以来就存在的std::thread开始。您可能已经知道;std::thread在其构造函数中接受一个可调用对象。它可能是一个普通的函数指针、一个functor或者一个lambda表达式。一个简单的例子:
#include <iostream>
#include <thread>
#include <vector>
#include <numeric> // for std::iota
int main()
{
const auto printThreadID = [](const char *str)
{
std::cout << str << ;: ;
<< std::this_thread::get_id() << ; thread id
;;
};
std::vector<int> numbers(100);
std::thread iotaThread([&numbers, &printThreadID](int startArg){
std::iota(numbers.begin(), numbers.end(), startArg);
printThreadID(;iota in; );
},10);
iotaThread.join();
printThreadID(;printing numbers in;);
for (const auto &num : numbers)
std::cout << num << ;, ;;
}