C++ Lambda 表达式实战入门与进阶(含车机项目示例)

适合已经在写 C++ 项目(尤其是 UI/车机代码)的同学,想系统上手 lambda 的用法与最佳实践。


一、lambda 是什么?一句话直观理解

lambda 就是“能写在函数内部的小函数”

传统做法:

uint32 GetTitleColor(uint8 daymode)
{
    return (daymode == 0)
        ? SKUI_COLOR_COMMON_LIGHT_BLACK_90
        : SKUI_COLOR_COMMON_NIGHT_WHITE_90;
}

lambda 写法(写在函数里面):

auto getTitleColor = [](uint8 daymode) -> uint32 {
    return (daymode == 0)
        ? SKUI_COLOR_COMMON_LIGHT_BLACK_90
        : SKUI_COLOR_COMMON_NIGHT_WHITE_90;
};

uint32 color = getTitleColor(daymode);

本质上:多了一个更灵活的语法,让我们可以在“用的地方”就就地定义一个小函数,不用跑到文件上方去写一堆 static 工具函数。


二、lambda 的完整语法长啥样?

标准形式:

[capture](parameter_list) mutable noexcept -> return_type {
    // 函数体
}

大部分时候可以省略很多东西,常见简化写法:

[]        // 捕获列表([] 表示不捕获外部变量)
(int x, int y)  // 参数列表
{        // 函数体开始
    return x + y;
}        // 函数体结束

实际工程里最常见的一种形态:

auto func = [](int x) {
    return x * 2;
};

三、lambda 的几个核心“组成部分”

1. 捕获列表([]

决定 lambda 能访问哪些外部变量,写在最前面。

  • []:不捕获任何外部变量,只能用参数和局部变量。
  • [&]:以引用方式捕获当前作用域中的所有变量。
  • [=]:以值拷贝方式捕获当前作用域中的所有变量。
  • [&x, y]:按引用捕获 x,按值捕获 y
  • [this]:捕获当前对象指针,在成员函数里常用。

在你项目里常见的简单写法:

auto getTitleColor = [](uint8 mode) -> uint32 {
    return (mode == 0)
        ? SKUI_COLOR_COMMON_LIGHT_BLACK_90
        : SKUI_COLOR_COMMON_NIGHT_WHITE_90;
};

这里的 [] 表示:不依赖外部任何变量,完全靠传进来的参数 mode

建议:如果不是必须,优先用 [] 或显式写 [&x, y],不要随手写 [&],容易引入意料之外的引用。


2. 参数列表 (...)

跟普通函数一样,比如:

[](int a, float b) { return a + b; }
  • 如果没有参数,可以写 () 或省略参数名:
[]() { /* ... */ }
  • 支持默认参数,但一般不推荐太花哨。

3. 返回类型 -> return_type

很多时候可以省略,由编译器自动推导:

auto f = [](int x) {
    return x * 2;  // 自动推断返回类型为 int
};

需要写 -> 的典型场景:

  • 返回类型比较复杂(如智能指针、容器迭代器等)。
  • 有多条 return 且类型不一致,写清楚避免推导歧义。

在你项目里的颜色 lambda 明确写返回类型:

auto getTitleColor = [](uint8 mode) -> uint32 {
    return (mode == 0)
        ? SKUI_COLOR_COMMON_LIGHT_BLACK_90
        : SKUI_COLOR_COMMON_NIGHT_WHITE_90;
};

4. 函数体 { ... }

就是一段普通 C++ 代码,里面可以:

  • 声明临时变量
  • if/for/while
  • 调用其他函数
  • 返回值 return ...

唯一要注意:能不能访问某个外部变量,取决于捕获列表怎么写


四、项目实战:用 lambda 清理 UI 代码中的重复逻辑

以你现在的 MenuMusicWindow 为例,之前的代码是:

uint32 titleColor  = (daymode == 0) ? SKUI_COLOR_COMMON_LIGHT_BLACK_90 : SKUI_COLOR_COMMON_NIGHT_WHITE_90;
uint32 singerColor = (daymode == 0) ? SKUI_COLOR_COMMON_LIGHT_BLACK_60 : SKUI_COLOR_COMMON_NIGHT_WHITE_60;

后来我们改成了 lambda:

auto getTitleColor = [](uint8 mode) -> uint32 {
    return (mode == 0)
        ? SKUI_COLOR_COMMON_LIGHT_BLACK_90
        : SKUI_COLOR_COMMON_NIGHT_WHITE_90;
};

auto getSingerColor = [](uint8 mode) -> uint32 {
    return (mode == 0)
        ? SKUI_COLOR_COMMON_LIGHT_BLACK_60
        : SKUI_COLOR_COMMON_NIGHT_WHITE_60;
};

uint32 titleColor  = getTitleColor(daymode);
uint32 singerColor = getSingerColor(daymode);

好处:

  • 同一类逻辑(“根据 daymode 选颜色”)只写一处,以后改颜色只改 lambda
  • 函数体更易读:逻辑从“细节(颜色常量)”提升到“意图(获取标题颜色)”。

DrawContent 中也用了类似的 lambda:

auto getInfoColor = [](uint8 mode) -> uint32 {
    return (mode == 0)
        ? SKUI_COLOR_COMMON_LIGHT_BLACK_90
        : SKUI_COLOR_COMMON_NIGHT_WHITE_90;
};

if (mediaSource == 0x0)
{
    DrawMenuText("No media information",
                 1420, 328 + m_s32OffsetY,
                 328, 40,
                 getInfoColor(daymode),  // 这里直接用 lambda
                 ...
                 m_u8Alpha);
}

五、和你项目风格贴近的几个常见用法

用法 1:替代“小工具函数”,作用域只在当前函数

你之前在车况界面的 DrawAlarm() 里已经用到过 lambda:

auto showIf = [](boolean condition, IconIndex index)
{
    if (condition)
    {
        const MenuCarConditionInfo& info = kIcons[index];
        SetTextureXY(info);
        DrawMesh(info.pcIcon, 0, nullptr, nullptr, FALSE, FALSE, s_alpha);
    }
};

这就等价于在外面写一个:

static void ShowIconIf(boolean condition, IconIndex index);

但:

  • lambda 版本只在这个函数里可见,不会污染整个文件的命名空间;
  • 看代码时也能“就近看到实现”,减少跳转。
用法 2:结合标准库算法(以后你熟悉了可以尝试)

最常见的语法就是:

std::sort(vec.begin(), vec.end(), [](const Item& a, const Item& b) {
    return a.value < b.value;
});

这里 lambda 就是“排序规则”,写得非常紧凑,不用单独写一个 CompareItem 函数。

你现在项目里暂时用得不多,但掌握了语法之后,用标准库算法写很多逻辑会非常爽


六、捕获外部变量的典型场景(给你一个直觉)

虽然你目前用的都是 [](不捕获),但未来你会遇到这种:

int baseX = 1420;
int offsetY = m_s32OffsetY;

auto drawLabel = [=](const char* text, int y) {
    DrawMenuText(text,
                 baseX, y + offsetY,
                 328, 40,
                 ...);
};
  • [=]:以“值拷贝”的方式捕获当前作用域中的变量(如 baseXoffsetY)。
  • 这样 drawLabel 里就可以直接用 baseXoffsetY,不需要每个参数都传一遍。

但一定要记住:捕获 = 引入更多外部依赖,调试时要多注意“这个 lambda 依赖了哪些状态”。


七、性能问题:lambda 会不会很慢?

在你这种 UI 项目场景下,不用担心

  • 对编译器来说,lambda 本质上就是个“语法糖”,绝大部分都会被内联成普通代码。
  • 不捕获或者简单捕获的 lambda,在 O2/O3 优化下,汇编基本和手写小函数一致。
  • 真正影响性能的是:绘制、IO、锁、复杂算法,而不是这点 lambda 语法。

结论:
在你的项目(车机 UI、菜单绘制)这种规模下,放心用 lambda,优先考虑可读性和可维护性


八、给你一个“最小可记忆模板”

以后你要写一个简单 lambda,可以直接按这个模板敲:

auto 名字 = [](参数列表) -> 返回类型 {
    // 函数体
};

例如获取颜色:

auto getTitleColor = [](uint8 mode) -> uint32 {
    return (mode == 0) ? COLOR_DAY : COLOR_NIGHT;
};

如果返回类型简单,就可以连 -> uint32 都省略:

auto getTitleColor = [](uint8 mode) {
    return (mode == 0) ? COLOR_DAY : COLOR_NIGHT;
};

更多推荐