代码存储位置:shanchuann/Modern_CPP

+ 辅助推导

auto* p = +[]{return 6;};
这是一个非捕获lambda,可以生成转换函数转换为函数指针,这里的一元+是为了辅助推导,是为了创造合适的语境。自然理解为使用转换函数返回函数指针。
此处为int(*)(),即无参、返回int的函数指针, +强制触发这个转换,将lambda直接转为函数指针类型,而非lambda自身的匿名类型。

1
2
int* p = nullptr;
+p;

+对指针无算术意义,仅将指针(左值)转换为同值的纯右值(prvalue),指针的指向和数值均不改变,属于无副作用的类型转换。
因为右边表达式的结果是函数指针类型int(*)(), auto 会自动推导 p 的类型为int(*)(),与 auto*的推导结果完全一致;*仅为显式强调指针类型以提升可读性,并非语法必需。

ODR

lambda(隐式或显式)捕获的任何实体均被该lambda表达式ODR使用,所有隐式捕获的变量必须在lambda表达式的可达作用域内声明。即使外部有再多的对象,没有ODR使用就不会被捕获(个别情况例外)。

1
2
3
4
float x;
float& r = x;
auto p = [=]{};
cout << sizeof p << endl; //1

什么是ODR?
ODR 是 One Definition Rule(单一定义规则) 的缩写,它规定程序中的每个实体(如函数、类、变量等)在整个程序的所有源文件中,只能有且仅有一个定义,否则会导致编译或链接错误。

另一方面,如果 captures 包含 capture-default 并且没有显式捕获封闭对象(作为 this 或 this),或者在 lambda 函数体中可 ODR 使用的自动变量,或者一个对应的变量具有自动存储期的结构化绑定(C++20 起),则如果该实体在表达式(包括在使用非静态类成员之前隐式添加 this-> 时)的潜在求值表达式中命名,则它会隐式*捕获该实体。

泛型lambda

泛型 lambda(Generic Lambda)是对 C++11 lambda 表达式的重要扩展,其核心特性是允许使用auto作为参数类型,从而让 lambda 具备类似模板函数的泛型能力,可灵活处理多种类型的参数。

基本概念

C++11 的 lambda 表达式要求显式指定参数类型(如intdouble),因此只能处理固定类型的参数;而 C++14 的泛型 lambda 通过auto声明参数,编译器会根据传入的实参自动推导参数类型,从而支持多种类型的输入。

例如,一个简单的泛型 lambda:

1
2
3
4
// 泛型lambda:参数x的类型由实参推导
auto print = [](auto x) {
std::cout << x << std::endl;
};

这个print可以接受intstringdouble等任意可输出的类型:

1
2
3
4
print(42);          // 推导x为int,输出:42
print(3.14); // 推导x为double,输出:3.14
print("hello"); // 推导x为const char*,输出:hello
print(std::string("world")); // 推导x为string,输出:world

实现原理

编译器会将泛型 lambda 转换为一个匿名函数对象(functor),其operator()运算符会被定义为模板函数。例如,上面的print lambda 会被编译器转换为类似以下的结构:

1
2
3
4
5
6
7
8
9
10
11
// 编译器生成的匿名函数对象(伪代码)
struct AnonymousLambda {
// 模板化的operator(),支持任意类型T
template <typename T>
void operator()(T x) const {
std::cout << x << std::endl;
}
};

// 用匿名对象初始化print
auto print = AnonymousLambda{};

正是因为operator()是模板函数,泛型 lambda 才能像模板一样处理多种类型。

常见用法

多参数泛型 lambda

泛型 lambda 可以有多个auto参数,分别推导类型:

1
2
3
4
5
6
7
8
// 实现任意类型的加法
auto add = [](auto a, auto b) {
return a + b;
};

add(1, 2); // 推导为int+int,返回3
add(1.5, 2.5); // 推导为double+double,返回4.0
add(std::string("a"), "b"); // 推导为string+const char*,返回"ab"

结合捕获列表

泛型 lambda 可以像普通 lambda 一样捕获外部变量,同时保持泛型能力:

1
2
3
4
5
6
7
8
9
int scale = 3;
// 捕获scale,对任意类型x执行x * scale
auto multiply = [scale](auto x) {
return x * scale;
};

multiply(2); // int:2*3=6
multiply(2.5); // double:2.5*3=7.5
multiply('a'); // char:'a'*3(ASCII值97*3=291,对应字符)

参数带修饰符(&引用、const 等)

泛型参数可以结合&(引用)、const等修饰符,实现更精细的类型控制:

1
2
3
4
5
6
7
8
9
10
// 接受左值引用,修改原变量
auto increment = [](auto& x) {
x++;
};

int a = 5;
increment(a); // a变为6

double b = 3.0;
increment(b); // b变为4.0

甚至可以使用通用引用(auto&&) 实现完美转发:

1
2
3
4
// 完美转发参数x到另一个函数
auto forwarder = [](auto&& x) {
some_function(std::forward<decltype(x)>(x)); // 保持x的左值/右值属性
};

与标准算法结合

泛型 lambda 在标准算法(如std::for_eachstd::transform)中非常实用,可避免为不同类型重复定义 lambda:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <vector>
#include <algorithm>

int main() {
std::vector<int> ints = {1, 2, 3};
std::vector<double> doubles = {1.1, 2.2, 3.3};

// 泛型lambda:打印任意类型的元素
auto print = [](auto x) {
std::cout << x << " ";
};

// 对int容器遍历
std::for_each(ints.begin(), ints.end(), print); // 输出:1 2 3
// 对double容器遍历
std::for_each(doubles.begin(), doubles.end(), print); // 输出:1.1 2.2 3.3
return 0;
}

lambda的捕获

静态局部变量

1
2
3
4
5
static int a = 42;
auto p = [=]{++a;};
cout<<sizeof p<<endl; //1
p();
return a; //43

image.png

如果满足

  • 该变量是非局部变量
  • 或具有静态或线程局部存储期(此时无法捕获该变量)
  • 或该变量以常量表达式初始化的引用。

那么lambda表达式在使用它前不需要先捕获。

因为a具有静态存储期,故auto p = [=]{++a;};无法捕获到变量,因此他是一个空类,p的大小自然为1。

常量表达式初始化

1
2
3
4
5
6
7
8
int main() {
const int N = 10;
auto p = [=] {
int arr[N]{};
};
cout << sizeof p << endl; // 1
return 0;
}

如果满足:

  • 该变量具有const而非volatile的整型或枚举类型,并已用**[常量表达式](常量表达式 - cppreference.cn - C++参考手册)初始化**。
  • 或者该变量时constexpr的且没有mutable成员(这表示即使不使用[=]捕获也可以直接使用)。

则lambda表达式在读取它的值前不需要先捕获。N没有被ODR使用,未被捕获,故输出为1。

存储期:程序中所有对象都具有以下存储期之一

存储期
自动(automatic)存储期 这类对象的存储在外围代码块开始时分配,并在结束时解分配。未声明为static、extern或thread_local的所有局部变量均有此类存储期
静态(static)存储期 这类对象的存储在程序开始时分配,并在程序结束时解分配。这类对象只存在一个实例。所有在命名空间(包括全局命名空间)作用域声明的对象,加上声明带有static或extern的对象拥有此存储期。
线程(thread)存储期 这类对象的存储在线程开始时分配,并在线程结束时解分配,每个线程拥有它自身的对象实例,只有声明为thread_local的对象拥有此存储期。thread_local可以与static或extern一同出现,他们用于调整链接。
动态(dynamic)存储期 这类对象的存储是通过使用动态内存分配函数来按请求分配和解分配的。

潜在求值表达式

如果 captures 包含 capture-default 并且没有显式捕获封闭对象(作为 this 或 this),或者在 lambda 函数体中可 ODR 使用的自动变量,或者一个对应的变量具有自动存储期的结构化绑定(C++20 起),则如果该实体在表达式(包括在使用非静态类成员之前隐式添加 this-> 时)的潜在求值表达式中命名,则它会隐式*捕获该实体。

为了确定隐式捕获,typeid 从不被视为使其操作数未求值。

即使实体只在 lambda 函数体实例化后被丢弃语句中命名,也可能被隐式捕获。(C++17 起)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void f(int, const int(&)[2] = {}) {}   // #1
void f(const int&, const int(&)[1]) {} // #2

void test()
{
const int x = 17;

auto l0 = [] { f(x); }; // OK: calls #1, does not capture x
auto g0 = [](auto a) { f(x); }; // same as above
cout << sizeof(l0) << ' ' << sizeof(g0) << endl;
auto l1 = [=] { f(x); }; // OK: captures x (since P0588R1) and calls #1
// the capture can be optimized away
auto g1 = [=](auto a) { f(x); }; // same as above
cout << sizeof(l1) << ' ' << sizeof(g1) << endl;
auto ltid = [=] { typeid(x); }; // OK: captures x (since P0588R1)
// even though x is unevaluated
// the capture can be optimized away
cout << sizeof(ltid) << endl;
auto g2 = [=](auto a)
{
int selector[sizeof(a) == 1 ? 1 : 2] = {};
f(x, selector); // OK: is a dependent expression, so captures x
};
cout << sizeof(g2) << endl;
auto g3 = [=](auto a)
{
typeid(a + x); // captures x regardless of
// whether a + x is an unevaluated operand
};
cout << sizeof(g3) << endl;
constexpr NoncopyableLiteralType w{ 42 };
auto l4 = [] { return w.n_; }; // OK: w is not odr-used, capture is unnecessary
// auto l5 = [=]{ return w.n_; }; // error: w needs to be captured by copy
cout << sizeof(l4) << endl;
}

输出

1
2
3
4
5
6
1 1
4 4
4
4
4
1

判断是否捕获除了是否ODR使用外,还有一个规则:该实体在取决于某个泛型 lambda 形参的**(C++17 前)表达式内的潜在求值表达式中被指名(包括在使用非静态类成员的前添加隐含的this->)。就此目的而言,始终认为typeid的操作数被潜在求值。即使实体仅在舍弃语句中被指名,它也可能会被隐式捕获。(C++17 起)**

用法讲解

继承lambda

继承 lambda并不是直接继承 lambda,而是通过模板类overloaded继承多个 lambda,实现重载operator()的效果。

1
2
3
4
5
template<class... Ts>
struct overloaded : Ts... // 继承所有Ts类型(这里Ts是各个lambda的闭包类型)
{
using Ts::operator()...; // 将所有基类(lambda)的operator()引入当前类
};

继承多个 lambda 的闭包类型每个 lambda 表达式都会生成一个独特的闭包类型,而overloaded通过可变参数模板Ts...继承了所有这些闭包类型。例如,当传入[](int){}, [](double){}时,Ts...就是这两个 lambda 的闭包类型,overloaded会同时继承这两个类型。

using Ts::operator()...的作用C++17 允许在using声明中使用参数包展开(...),这行代码的效果是:将所有基类(每个 lambda)的operator()都 “引入” 到overloaded类中。由于每个 lambda 的operator()参数列表不同(比如一个接收int,一个接收double),这些operator()会在overloaded中形成重载关系,从而实现 “根据参数类型自动匹配对应 lambda” 的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//继承lambda
template<class... Ts>
struct overloaded : Ts...
{
using Ts::operator()...;
};

int main() {
auto c = overloaded{ [](int arg) { std::cout << arg << endl; },
[](double arg) { std::cout << arg << endl; },
[](auto arg) { std::cout << arg << endl
; }
};
c(10);
c(3.14);
c("你好,C++");
}
  • C++20 环境:编译器会自动推导overloaded的模板参数(Ts...会匹配三个 lambda 的闭包类型),无需额外代码,直接编译通过。

  • C++17 环境:需手动添加推导指引,否则编译器无法确定overloaded的模板参数:

    1
    2
    3
    // 必须添加这行,告诉编译器如何推导模板参数
    template<class... Ts>
    overloaded(Ts...) -> overloaded<Ts...>;

    缺少这行时,C++17 会报错(如 “无法推导overloaded的模板参数”)。

用户定义推导指引

用户定义推导指引(UDDG)是 C++17 及以后引入的类模板参数推导(CTAD)补充工具,核心作用是当类模板的隐式推导指引(由构造函数自动生成)无法满足需求时,手动告诉编译器 “如何从构造参数推导出类模板的具体类型”,解决隐式推导歧义或失效的问题。

定位与作用

解决的痛点:当类模板的构造参数(如迭代器范围)与模板参数(如容器元素类型)无直接对应关系时,隐式指引无法推导,UDDG 可补充推导逻辑。

本质分工:仅负责 “推导类模板类型”,不参与实际构造执行 —— 推导结果确定后,仍由类的构造函数完成对象初始化。

核心语法

UDDG 语法基于 “模拟构造函数参数,指定推导结果”,关键要素仅 4 个:

1
2
3
4
// 通用形式(C++17/20)
[template <模板参数列表>] // 可选,需推导参数时加(如Iter)
[explicit] // 可选,控制仅在直接初始化中生效
类模板名(构造参数列表) -> 类模板具体类型; // 核心:参数→类型的映射
  • 类模板名:必须与待推导的类模板同名(如containerA)。
  • 构造参数列表:模拟实际构造时的参数类型(如Iter b, Iter e),不能用auto等占位符类型。
  • 推导结果(-> 后):明确最终推导出的类模板类型(如container<typename Iter::value_type>)。
  • explicit:带此关键字时,指引仅在 “直接初始化”(如A a{i,i})中生效,“复制初始化”(如A a = {i,i})中忽略。

lambda捕获子句包展开

1
2
3
4
5
6
7
8
9
10
template<class... Args>
void f(Args... args) {
auto lm = [&args...] { };
auto lm2 = [&] { };
std::cout << sizeof lm << '\n';
std::cout << sizeof lm2 << '\n';
}
int main() {
f(1, 1.0, 1.f);
}
lambda 捕获子句 捕获行为 闭包存储内容 大小(64 位系统)
lm [&args...] 显式捕获参数包中所有元素的引用(无论是否使用) 3 个引用(int&, double&, float&) 24 字节
lm2 [&] 默认引用捕获,但因函数体未使用任何变量,实际不捕获任何内容 1 字节(空类)

new一个带捕获lambda

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
int main() {
auto lambda = [x = 0](int c) {std::cout << c << std::endl; };
auto pLambda = std::make_shared<decltype(lambda)>(lambda);
(*pLambda)(10);

auto p = new auto([x = 0](int c) {std::cout << c << std::endl; });
(*p)(10);
delete p;
}
//10
//10

返回的指针是指向了lambda类的对象,自然要先*然后调用operator(),如果你对new auto()这个组合有疑问

  • 类型new类型 中使用了占位符,即 auto 或 decltype(auto) (C++14 起),可能与类型约束结合 (C++20 起)
1
auto p = new auto(5.6);
  • 捕获的变量(这里x = 0)会成为闭包类型的私有成员变量,闭包对象因此拥有 “状态”;
  • 这种闭包类型是匿名的(无法显式写出类型名),只能通过decltypeauto推导。