10 / 80 · C++11 · 约 8 分钟
宏与 inline:文本替换不等于函数调用
函数式宏在预处理阶段替换记号,不提供参数类型、正常作用域或单次求值保证。inline 函数具有完整函数语义,主要关联多翻译单元定义规则,并不强制机器码内联;常量和计算通常优先使用 constexpr 或模板。
展开之后才有表达式
宏 #define BAD_DOUBLE(x) x + x 接收的是预处理记号。3 * BAD_DOUBLE(1 + 2) 展开成 3 * 1 + 2 + 1 + 2,结果是 8 而非期望的 18。给参数和整体加括号能修正这个优先级问题,却不能解决重复求值。
例如把参数出现两次的宏用于自增表达式,可能导致副作用次数变化甚至未定义行为,因此示例只用无副作用字面量展示错误。宏也不遵守 C++ 命名空间作用域;碰巧同名的成员或库函数记号都可能被预处理器替换。
函数保留类型和调用边界
普通函数或函数模板按语言规则接收参数,类型检查和重载决议在调用处发生。示例中的倍增函数按值接收一个 int,调用 twice(++n) 时自增只发生一次,函数体复用的是参数值,而不是重复粘贴调用者的表达式。
constexpr 允许满足条件的调用参与常量求值,并不要求每次调用都发生在编译期。它适合替换表达式常量和小型纯计算宏;模板适合保留类型泛化。条件编译、字符串化和记号拼接则仍是预处理器擅长的不同任务。
inline 的重要承诺是定义规则
inline 不是“强制消除调用开销”。编译器可以不内联一个 inline 函数,也可以内联一个没写 inline 的函数。对于通常的头文件代码,inline 允许符合单一定义规则的同一函数定义出现在多个翻译单元中,同时仍表示同一个实体。
多份定义需要满足 ODR 条件,不能让不同源文件用不同宏把函数体改成不同实现。constexpr 函数隐含 inline;普通头文件场景中类内定义的成员函数也隐含 inline,命名模块另有规则。把性能问题归结为有没有写这个关键字,通常会忽略真正的优化上下文。
容易答错的地方
- 宏参数整体加括号只能修复结合方式,不能保证参数只求值一次。
- inline 不允许不同翻译单元提供互不一致的函数定义;这种 ODR 违规可能没有可靠诊断。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
#define BAD_DOUBLE(x) x + x
constexpr int twice(int value) { return value + value; }
int main() {
const int expanded = 3 * BAD_DOUBLE(1 + 2);
const int called = 3 * twice(1 + 2);
int n = 2;
const int one_evaluation = twice(++n);
assert(expanded == 8);
assert(called == 18);
assert(n == 3 && one_evaluation == 6);
std::cout << expanded << ' ' << called << ' ' << n << '\n';
}
#undef BAD_DOUBLE
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-macros-inline.cpp -o example && ./example预期结果
8 18 3
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
把 #define SQUARE(x) ((x) * (x)) 替换为保持单次实参求值的 C++11 函数,并说明还剩什么算术前提。
查看参考答案
可写 constexpr int square(int x) { return x * x; }。square(++n) 会先把一次自增结果作为值传入,再使用该值两次。但 int 乘法仍可能有符号溢出;接口必须限制输入范围,或改用有检查的算法。替换宏消除了重复求值,不会自动消除数值溢出。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。