C++ 中立即调用函数表达式(IIFE)的用途
立即调用函数表达式(Immediately Invoked Function Expression, IIFE)是一个在源代码中被同时定义和调用的 lambda。在 C++ 里的语法是:
[]{...}();
这种结构把一串语句变成一个表达式,从而为系统程序员打开了两个真正有用的模式。
复杂的初始化
C++ 把成员初始化器当作表达式求值。当初始化逻辑不够平凡时,自然的选择是辅助函数或三目链。IIFE 提供了第三种选项:让逻辑保持在内联位置,同时不用临时变量污染外层作用域。
初始化 const 变量
const int threshold = [&] {
int base = read_config("threshold");
if (base <= 0) base = DEFAULT_THRESHOLD;
return base;
}();
因为 threshold 是 const,编译器能对它做更激进的推理。如果不用 IIFE,想达到同样效果要么用可变变量、要么写一个独立的辅助函数。
初始化类成员
struct Connection {
int fd_;
Connection(const char* host, uint16_t port)
: fd_([&] {
int s = socket(AF_INET, SOCK_STREAM, 0);
if (s == -1) throw std::system_error(errno, std::generic_category());
// 设置选项 ...
return s;
}()) {}
};
这避免了”仅仅为了满足成员初始化列表语法”而创建静态成员函数。配合委托构造函数,它几乎消除了这类辅助函数存在的大多数理由。
C++ Core Guidelines 明确支持这一模式:
ES.28:用 lambda 做复杂的初始化,尤其是 const 变量的初始化。
把冷代码驱逐出热路径
GCC 和 Clang 会自动把抛异常的代码放到一个单独的冷区段(.text.unlikely)里,因为异常路径被假定是罕见的。而通过返回值处理错误的代码,默认并不享受这种待遇:
if (send_message() == -1) {
log_error("send failed");
return;
}
错误处理块与快路径内联并置,占用着指令缓存行 —— 而 CPU 会在热循环的每次迭代中预取这些行。
使用 [[unlikely]](C++20)
if (send_message() == -1) [[unlikely]] {
log_error("send failed");
return;
}
这给分支预测器一个提示,可能会重排基本块,但不保证代码被放置到 out-of-line 的位置。
用 IIFE 搭配 GCC 属性
if (send_message() == -1) {
[&]() __attribute__((noinline, cold)) {
log_error("send failed");
}();
return;
}
两个属性做着不同的事:
noinline阻止编译器把 lambda 体重新内联回调用方。cold告诉编译器这个函数不大可能被执行。编译器会按体积而非速度来优化它,链接器会把它放到二进制的冷区段。
结果是错误处理器与热路径在物理上被分开。分支预测得到改善,因为 CPU 看到的是一条几乎永远不落入的分支,热循环所使用的指令缓存行也不再包含错误处理代码。
这个技巧归功于 Matt Godbolt。你可以在 Compiler Explorer 上对比加不加这些属性的汇编差异。
替代方案
| 方案 | 优点 | 缺点 |
|---|---|---|
三目 ?: | 标准、到处能用 | 只能写单个表达式 |
| GCC 语句表达式 | 允许多条语句 | 非标准扩展 |
| 独立的辅助函数 | 总是能用 | 破坏局部性,多出一个函数声明 |
| IIFE | 内联且为标准 C++11 | lambda 语法对部分读者不熟悉 |
参考
- C++ Core Guidelines, ES.28: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-lambda-init
- GCC Common Function Attributes: https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
- Matt Godbolt, “Forcing code out of line in GCC and C++11”: https://xania.org/201209/forcing-code-out-of-line-in-gcc