C++ STL之数值算法与Lambda详解:从使用到底层,再到面试八股

C++ STL之数值算法与Lambda详解:从使用到底层,再到面试八股

本文面向面试和日常开发,先讲调用,再讲原理,最后给口语化面试答案。


一、用法速查

1.1 <numeric> 数值算法

accumulate ------ 左折叠累加

std::accumulate 对区间做左折叠:((a+b)+c)+d。O(n),要求二元运算,不要求可结合性。

cpp 复制代码
#include <numeric>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{1, 2, 3, 4, 5};

    int sum = accumulate(v.begin(), v.end(), 0);
    cout << sum << "\n";                    // 15

    int prod = accumulate(v.begin(), v.end(), 1, multiplies<int>());
    cout << prod << "\n";                   // 120

    vector<string> vs{"a", "b", "c"};
    string s = accumulate(vs.begin(), vs.end(), string(""),
        [](string &acc, string &cur) { return acc + "-" + cur; });
    cout << s << "\n";                      // -a-b-c
}

陷阱:int overflow。 accumulate(v.begin(), v.end(), 0) 的初值 0int 类型,推导出的累加类型是 int。哪怕 v 里放的是 long long,结果也会在溢出后截断:

cpp 复制代码
vector<long long> v{10000000000LL, 20000000000LL};
auto sum = accumulate(v.begin(), v.end(), 0);      // int 溢出!结果错误
auto sum2 = accumulate(v.begin(), v.end(), 0LL);   // 正确:初值 long long

reduce ------ 可并行归约(C++17)

std::reduce 要求运算是结合的可交换的(半群),因此可以按二叉树结构归约,支持并行策略。

cpp 复制代码
#include <execution>
#include <numeric>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<long long> v(1000000, 1);

    auto s1 = reduce(v.begin(), v.end(), 0LL);
    auto s2 = reduce(execution::par_unseq, v.begin(), v.end(), 0LL);

    cout << s1 << " " << s2 << "\n";        // 1000000 1000000
}

不满足结合律的运算不能用 reduce: 浮点数加法不结合((a+b)+c != a+(b+c) 在小数和大数相加时),所以 par_unseq reduce 对浮点数的结果是非确定性的


inner_product ------ 内积

两个区间对应元素相乘后累加,O(n)。

cpp 复制代码
#include <numeric>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> a{1, 2, 3};
    vector<int> b{4, 5, 6};

    int dot = inner_product(a.begin(), a.end(), b.begin(), 0);
    cout << dot << "\n";                    // 32

    int same = inner_product(a.begin(), a.end(), b.begin(), 0,
        plus<int>(), equal_to<int>());
    cout << same << "\n";                   // 0
}

adjacent_difference ------ 相邻差

a[1]-a[0], a[2]-a[1], ...,第一个元素原样输出。

cpp 复制代码
#include <numeric>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{1, 3, 6, 10, 15};
    vector<int> diff(v.size());

    adjacent_difference(v.begin(), v.end(), diff.begin());
    for (int x : diff) cout << x << " ";    // 1 2 3 4 5
    cout << "\n";

    vector<double> sales{100, 150, 200, 180};
    vector<double> growth(sales.size());
    adjacent_difference(sales.begin(), sales.end(), growth.begin(),
        [](double cur, double prev) { return (cur - prev) / prev; });
    for (double x : growth) cout << x << " ";   // 100 0.5 0.333 -0.1
    cout << "\n";
}

partial_sum ------ 前缀和

a[0], a[0]+a[1], a[0]+a[1]+a[2], ...

cpp 复制代码
#include <numeric>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{1, 2, 3, 4, 5};
    vector<int> ps(v.size());

    partial_sum(v.begin(), v.end(), ps.begin());
    for (int x : ps) cout << x << " ";      // 1 3 6 10 15
    cout << "\n";

    vector<int> fact(v.size());
    partial_sum(v.begin(), v.end(), fact.begin(), multiplies<int>());
    for (int x : fact) cout << x << " ";    // 1 2 6 24 120
    cout << "\n";
}

iota ------ 连续赋值

[first, last) 依次赋值为 value, value+1, value+2, ...

cpp 复制代码
#include <numeric>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v(5);
    iota(v.begin(), v.end(), 1);
    for (int x : v) cout << x << " ";       // 1 2 3 4 5
    cout << "\n";

    vector<int> idx(10);
    iota(idx.begin(), idx.end(), 0);        // 0 1 2 ... 9
}

1.2 Lambda 表达式

基本语法
cpp 复制代码
[捕获](参数) -> 返回类型 { 主体 }
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    auto hello = [] { cout << "Hello\n"; };
    hello();

    auto add = [](int a, int b) -> int { return a + b; };
    cout << add(3, 4) << "\n";              // 7

    auto sub = [](int a, int b) { return a - b; };
    cout << sub(10, 3) << "\n";             // 7

    vector<int> v{1, 2, 3, 4, 5, 6};
    auto it = remove_if(v.begin(), v.end(), [](int x) { return x % 2 != 0; });
    v.erase(it, v.end());
    for (int x : v) cout << x << " ";       // 2 4 6
    cout << "\n";
}
捕获方式
cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    int a = 1, b = 2;

    auto by_val = [=] { cout << a << " " << b << "\n"; };
    by_val();                               // 1 2
    a = 10;
    by_val();                               // 还是 1 2

    auto by_ref = [&] { cout << a << " " << b << "\n"; };
    by_ref();                               // 10 2

    auto mix = [=, &b] {
        b = 20;
        return a + b;
    };
    cout << mix() << "\n";                  // 21
}
mutable ------ 修改值捕获的副本

默认 Lambda 的 operator()const,值捕获的变量不能修改。加 mutable 解除限制------但只修改副本,不影响外部。

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    int cnt = 0;

    auto good = [=]() mutable { ++cnt; return cnt; };
    cout << good() << "\n";                 // 1
    cout << good() << "\n";                 // 2
    cout << cnt << "\n";                    // 0
}

实用场景:生成自增 ID 的函数对象。

cpp 复制代码
auto maker = [id = 0]() mutable { return ++id; };
cout << maker() << maker() << maker() << "\n";  // 123
初始化捕获(C++14)------ 移动不可复制对象进 Lambda
cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    auto p = make_unique<int>(42);

    auto task = [ptr = move(p)] {
        cout << *ptr << "\n";
    };
    task();                                 // 42
}
泛型 Lambda(C++14)

参数类型用 auto 推导,相当于隐式的模板化 operator()

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    auto print = [](const auto &x) { cout << x << " "; };

    vector<int> vi{1, 2, 3};
    for_each(vi.begin(), vi.end(), print);  // 1 2 3
    cout << "\n";

    vector<string> vs{"a", "b", "c"};
    for_each(vs.begin(), vs.end(), print);  // a b c
    cout << "\n";

    auto same = []<typename T>(const T &a, const T &b) { return a == b; };
    cout << same(1, 1) << "\n";             // 1
}

二、底层原理

2.1 accumulate 和 reduce 的本质差异

accumulatereduce 的根本区别在于对运算的要求不同

accumulate = 左折叠(left fold)。 从左到右依次处理每个元素,运算顺序固定为 ((((init + a₁) + a₂) + a₃) + ... )。不要求结合律和交换律,结果确定可复现。

reduce = 归约(reduction)。 要求二元运算是可结合的可交换的 ,即构成一个半群 。有了结合律,reduce 可以把区间拆成若干段,各自归约后合并结果------二叉树归约深度 O(log n),而非左折叠的 O(n)。有了交换律,par_unseq 可任意调换顺序。
#mermaid-svg-nYO69FtVDvTo5sjB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nYO69FtVDvTo5sjB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nYO69FtVDvTo5sjB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nYO69FtVDvTo5sjB .error-icon{fill:#552222;}#mermaid-svg-nYO69FtVDvTo5sjB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nYO69FtVDvTo5sjB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nYO69FtVDvTo5sjB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nYO69FtVDvTo5sjB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nYO69FtVDvTo5sjB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nYO69FtVDvTo5sjB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nYO69FtVDvTo5sjB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nYO69FtVDvTo5sjB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nYO69FtVDvTo5sjB .marker.cross{stroke:#333333;}#mermaid-svg-nYO69FtVDvTo5sjB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nYO69FtVDvTo5sjB p{margin:0;}#mermaid-svg-nYO69FtVDvTo5sjB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nYO69FtVDvTo5sjB .cluster-label text{fill:#333;}#mermaid-svg-nYO69FtVDvTo5sjB .cluster-label span{color:#333;}#mermaid-svg-nYO69FtVDvTo5sjB .cluster-label span p{background-color:transparent;}#mermaid-svg-nYO69FtVDvTo5sjB .label text,#mermaid-svg-nYO69FtVDvTo5sjB span{fill:#333;color:#333;}#mermaid-svg-nYO69FtVDvTo5sjB .node rect,#mermaid-svg-nYO69FtVDvTo5sjB .node circle,#mermaid-svg-nYO69FtVDvTo5sjB .node ellipse,#mermaid-svg-nYO69FtVDvTo5sjB .node polygon,#mermaid-svg-nYO69FtVDvTo5sjB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nYO69FtVDvTo5sjB .rough-node .label text,#mermaid-svg-nYO69FtVDvTo5sjB .node .label text,#mermaid-svg-nYO69FtVDvTo5sjB .image-shape .label,#mermaid-svg-nYO69FtVDvTo5sjB .icon-shape .label{text-anchor:middle;}#mermaid-svg-nYO69FtVDvTo5sjB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nYO69FtVDvTo5sjB .rough-node .label,#mermaid-svg-nYO69FtVDvTo5sjB .node .label,#mermaid-svg-nYO69FtVDvTo5sjB .image-shape .label,#mermaid-svg-nYO69FtVDvTo5sjB .icon-shape .label{text-align:center;}#mermaid-svg-nYO69FtVDvTo5sjB .node.clickable{cursor:pointer;}#mermaid-svg-nYO69FtVDvTo5sjB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nYO69FtVDvTo5sjB .arrowheadPath{fill:#333333;}#mermaid-svg-nYO69FtVDvTo5sjB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nYO69FtVDvTo5sjB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nYO69FtVDvTo5sjB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nYO69FtVDvTo5sjB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nYO69FtVDvTo5sjB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nYO69FtVDvTo5sjB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nYO69FtVDvTo5sjB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nYO69FtVDvTo5sjB .cluster text{fill:#333;}#mermaid-svg-nYO69FtVDvTo5sjB .cluster span{color:#333;}#mermaid-svg-nYO69FtVDvTo5sjB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nYO69FtVDvTo5sjB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nYO69FtVDvTo5sjB rect.text{fill:none;stroke-width:0;}#mermaid-svg-nYO69FtVDvTo5sjB .icon-shape,#mermaid-svg-nYO69FtVDvTo5sjB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nYO69FtVDvTo5sjB .icon-shape p,#mermaid-svg-nYO69FtVDvTo5sjB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nYO69FtVDvTo5sjB .icon-shape .label rect,#mermaid-svg-nYO69FtVDvTo5sjB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nYO69FtVDvTo5sjB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nYO69FtVDvTo5sjB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nYO69FtVDvTo5sjB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} accumulate

((a+b)+c)+d
串行链式

不能并行
reduce

(a+b)+(c+d)
二叉树归约

可并行

这就是为什么只有 reduce 可以配 execution::par_unseq------accumulate 的串行链式依赖不允许并发;reduce 的自由结合树天然适配 SIMD 和多线程拆分。

2.2 数值精度陷阱

accumulate 对大整数的溢出问题已经在 1.1 节提到:初值类型决定累加类型。更深层的问题是浮点数:

复制代码
float a = 1e8, b = 1, c = -1e8;
accumulate: (a + b) + c = 1e8 + (-1e8) = 0(a + b 溢出尾数精度,b 被截断)
algebraic:  a + (b + c) = 1e8 + 0 = 1e8

浮点数加法不满足结合律。accumulate 固定从左到右,对某些输入序列可能会产生较大的舍入误差。但 reduce 在浮点上更危险 ------因为 par_unseq 的归约顺序不确定,每次运行结果可能不同,导致非确定性 bug。

2.3 Lambda 的编译器展开

Lambda 不是"轻量级函数",而是匿名仿函数对象

cpp 复制代码
// 你写的:
auto add = [](int a, int b) { return a + b; };

// 编译器生成的类似代码:
struct __lambda_add {
    auto operator()(int a, int b) const { return a + b; }
};
__lambda_add add{};

关键点:

  1. 每个 Lambda 有唯一的 closure type。即使是完全相同代码的两个 Lambda,类型也不同:
cpp 复制代码
auto f1 = [](int x) { return x; };
auto f2 = [](int x) { return x; };
// f1 和 f2 类型不同!不能互相赋值
  1. operator() 默认是 const 。值捕获的变量在 operator() 内部是 const 引用,不能修改。加 mutable 后生成非 const 的 operator()

    // [=] 值捕获的展开示意:
    int a = 1;
    auto f = a mutable { ++a; };

    // 编译器生成:
    struct __lambda_f {
    int a;
    auto operator()() {
    return ++a;
    }
    };
    __lambda_f f{a};

  2. 捕获列表 = 成员变量[a] 生成一个 int a 成员;[&a] 生成一个 int &a 引用成员;[x = move(p)] 按移动后的值构造成员。

  3. 无捕获的 Lambda 可以转为函数指针

cpp 复制代码
using Fn = int(*)(int, int);
Fn p = [](int a, int b) { return a + b; };   // 无捕获→函数指针
int x = 0;
// Fn q = [x](int a, int b) { return a + b; };  // 编译错误

#mermaid-svg-qMnDsZDiXjnYIkFs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qMnDsZDiXjnYIkFs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qMnDsZDiXjnYIkFs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qMnDsZDiXjnYIkFs .error-icon{fill:#552222;}#mermaid-svg-qMnDsZDiXjnYIkFs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qMnDsZDiXjnYIkFs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qMnDsZDiXjnYIkFs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qMnDsZDiXjnYIkFs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qMnDsZDiXjnYIkFs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qMnDsZDiXjnYIkFs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qMnDsZDiXjnYIkFs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qMnDsZDiXjnYIkFs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qMnDsZDiXjnYIkFs .marker.cross{stroke:#333333;}#mermaid-svg-qMnDsZDiXjnYIkFs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qMnDsZDiXjnYIkFs p{margin:0;}#mermaid-svg-qMnDsZDiXjnYIkFs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qMnDsZDiXjnYIkFs .cluster-label text{fill:#333;}#mermaid-svg-qMnDsZDiXjnYIkFs .cluster-label span{color:#333;}#mermaid-svg-qMnDsZDiXjnYIkFs .cluster-label span p{background-color:transparent;}#mermaid-svg-qMnDsZDiXjnYIkFs .label text,#mermaid-svg-qMnDsZDiXjnYIkFs span{fill:#333;color:#333;}#mermaid-svg-qMnDsZDiXjnYIkFs .node rect,#mermaid-svg-qMnDsZDiXjnYIkFs .node circle,#mermaid-svg-qMnDsZDiXjnYIkFs .node ellipse,#mermaid-svg-qMnDsZDiXjnYIkFs .node polygon,#mermaid-svg-qMnDsZDiXjnYIkFs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qMnDsZDiXjnYIkFs .rough-node .label text,#mermaid-svg-qMnDsZDiXjnYIkFs .node .label text,#mermaid-svg-qMnDsZDiXjnYIkFs .image-shape .label,#mermaid-svg-qMnDsZDiXjnYIkFs .icon-shape .label{text-anchor:middle;}#mermaid-svg-qMnDsZDiXjnYIkFs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qMnDsZDiXjnYIkFs .rough-node .label,#mermaid-svg-qMnDsZDiXjnYIkFs .node .label,#mermaid-svg-qMnDsZDiXjnYIkFs .image-shape .label,#mermaid-svg-qMnDsZDiXjnYIkFs .icon-shape .label{text-align:center;}#mermaid-svg-qMnDsZDiXjnYIkFs .node.clickable{cursor:pointer;}#mermaid-svg-qMnDsZDiXjnYIkFs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qMnDsZDiXjnYIkFs .arrowheadPath{fill:#333333;}#mermaid-svg-qMnDsZDiXjnYIkFs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qMnDsZDiXjnYIkFs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qMnDsZDiXjnYIkFs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qMnDsZDiXjnYIkFs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qMnDsZDiXjnYIkFs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qMnDsZDiXjnYIkFs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qMnDsZDiXjnYIkFs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qMnDsZDiXjnYIkFs .cluster text{fill:#333;}#mermaid-svg-qMnDsZDiXjnYIkFs .cluster span{color:#333;}#mermaid-svg-qMnDsZDiXjnYIkFs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qMnDsZDiXjnYIkFs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qMnDsZDiXjnYIkFs rect.text{fill:none;stroke-width:0;}#mermaid-svg-qMnDsZDiXjnYIkFs .icon-shape,#mermaid-svg-qMnDsZDiXjnYIkFs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qMnDsZDiXjnYIkFs .icon-shape p,#mermaid-svg-qMnDsZDiXjnYIkFs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qMnDsZDiXjnYIkFs .icon-shape .label rect,#mermaid-svg-qMnDsZDiXjnYIkFs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qMnDsZDiXjnYIkFs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qMnDsZDiXjnYIkFs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qMnDsZDiXjnYIkFs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Lambda 表达式
闭包类型

唯一匿名类型
operator()

默认 const
mutable → 去掉 const
无捕获 → 可转函数指针

2.4 Lambda vs std::function

维度 Lambda std::function
类型 每个 Lambda 唯一 closure type 统一的类型擦除包装
调用开销 直调 operator(),可内联 虚函数表 / 函数指针间接调用,难以内联
构造开销 零额外,栈上对象 可能堆分配(大捕获),类型擦除有开销
灵活性 仅限单个闭包 可存储任意可调用对象,可赋值/重新绑定
cpp 复制代码
auto add = [](int a, int b) { return a + b; };
int r1 = add(1, 2);     // 编译为:call __lambda_add::operator()

function<int(int,int)> f = [](int a, int b) { return a + b; };
int r2 = f(1, 2);       // 编译为:间接调用

工程建议 :只在需要类型擦除(如回调注册、多态函数容器)时才用 std::function。普通场景直接用 auto 或模板接受 Lambda,零开销。


三、面试题 + 口语化答案

Q1:accumulate 和 reduce 有什么区别?

"核心区别是对运算的要求不同。accumulate 只要求二元运算,从左到右链式折叠,不能并行。reduce 要求可结合+可交换(半群),可以按二叉树归约,C++17 配 execution::par_unseq 并行加速。如果运算不满足结合律------比如浮点加法------accumulate 结果是确定的,reduce 在并行模式下是不确定的。"

Q2:Lambda 的 operator() 默认是 const 吗?怎么改?

"默认是 const 的。值捕获的变量在 Lambda 体内是只读的------想修改要用 mutable 关键字。加了 mutable 后,operator() 变成非 const 的,值捕获的副本可以修改,但不影响外部变量。"

Q3:[=][&] 的 dangling 陷阱是什么?

"[=] 值捕获做快照,不会 dangling。但 [&] 引用捕获只保存引用------如果 Lambda 生命周期超出被引用变量(比如 Lambda 存入容器或作为回调,而变量是栈上局部变量),引用变成野指针。安全的做法是:Lambda 只在本作用域内同步调用时用 [&],需要异步或存起来时用 [=] 或显式值捕获。"

Q4:泛型 Lambda 怎么写?和模板函数有什么区别?

cpp 复制代码
auto f = [](const auto &x, const auto &y) { return x + y; };

"C++14 引入。auto 参数相当于隐式生成了模板化的 operator(),每个参数类型独立推导。C++20 还可以写显式模板 Lambda []<typename T>(T x){...}。本质上就是一个简化了的模板仿函数。"

Q5:iota 的底层怎么实现?

"就是 while(first != last) { *first++ = value; ++value; }------每次把当前 value 赋给迭代器,然后 value 自增。名字来自 APL 语言的整数序列符号 ⍳。value 的自增类型由模板参数 T 决定,所以 iota(v.begin(), v.end(), 0) 是 int 递增,iota(v.begin(), v.end(), 0LL) 是 long long 递增。"

Q6:浮点数累加为什么不准?accumulate 和 reduce 谁更准?

"浮点数加法不满足结合律------(1e8 + 1) - 1e8 != 1e8 + (1 - 1e8)。大数和小数相加时,小数的尾数被截断。accumulate 从左到右顺序固定,结果确定,但不能避免误差。reduce 在并行时归约顺序不确定,每次运行结果可能不同------这对需要可复现结果的系统不可接受。工程上需要高精度累积时,用 Kahan summation 或 long double。"

Q7:为什么无捕获的 Lambda 可以转为函数指针,有捕获的不行?

"无捕获的 Lambda 的 closure type 可以生成一个 operator() 和一个同签名静态函数,后者可作独立函数指针传递。有捕获后 operator() 需要访问成员变量(捕获的值或引用),函数指针不可能携带额外上下文------所以无法直接转换。不过可以用 std::function 或模板来包装带捕获的 Lambda。"

Q8:std::function 包装 Lambda 有什么代价?

"三层代价。第一,构造开销std::function 做类型擦除,小对象可能栈上存储(SBO 优化),大对象堆分配。第二,调用开销 :每次调用走虚函数或函数指针间接跳转,编译器无法内联,调用比裸 Lambda 慢 2-3 倍是正常的。第三,体积std::function 本身至少 32-48 字节。除非需要存储不同类型的可调用对象,否则永远用 auto 或模板接收 Lambda。"


一句话总结<numeric> 算法的核心差异在于 accumulate(左折叠,串行确定)和 reduce(半群归约,可并行)对运算的不同要求;Lambda 本质是编译器生成的匿名仿函数,每个 Lambda 类型唯一,operator() 默认 const------理解这些底层机制,是你在面试中把"会用"和"懂原理"区分开来的关键。