4. constexpr函数实现编译期单位换算

constexpr函数实现编译期单位换算 学习卡片

为什么单位换算适合放进编译期

长度、时间这类换算的系数是固定的:1 km = 1000 m、1 h = 3600 s。如果换算函数是 constexpr,那么当输入是编译期常量时,结果也在编译期算出,运行时零开销;同时可以用 static_assert 把"1 km 必须等于 1000 m"这类事实写成编译期断言,一旦系数写错,编译直接失败,而不是等到运行期才发现。

关键约束有三点:函数体必须是 C++14 起允许的宽松 constexpr(可含局部变量、分支、循环);参数与返回类型必须是字面类型;static_assert 的表达式必须能在编译期求值,因此传入的实参本身也得是常量表达式。

constexpr 换算函数:同一份代码,两条求值路径 constexpr double km_to_m(double km) { return km * 1000.0; } constexpr double h_to_s(double h) { return h * 3600.0; } 常量实参 运行期变量 编译期求值路径 static_assert(km_to_m(1.0)==1000.0) static_assert(h_to_s(1.0)==3600.0) 系数正确 → 编译通过;系数写错 → 编译报错,运行期永不执行 运行期求值路径 double d = read_distance(); double m = km_to_m(d); 同一函数体被复用,只是求值时机推迟到运行期 验证要点 ① static_assert 的实参必须是常量表达式,否则编译报错;② 换算系数只写一处,编译期与运行期共用;③ 断言失败信息应能直接指出是哪个单位换算出错。

实现:长度与时间两组换算

下面给出一个可直接编译的单文件示例。长度以米为基准,时间以秒为基准,每个换算函数都是 constexpr,并用 static_assert 覆盖正向与反向换算。

#include <cassert>
#include <cstdint>

// ---------- 长度:基准单位 = 米 ----------
constexpr double km_to_m(double km)   { return km * 1000.0; }
constexpr double m_to_km(double m)    { return m / 1000.0; }
constexpr double cm_to_m(double cm)   { return cm / 100.0; }
constexpr double inch_to_m(double in) { return in * 0.0254; }

// ---------- 时间:基准单位 = 秒 ----------
constexpr double h_to_s(double h)  { return h * 3600.0; }
constexpr double min_to_s(double m){ return m * 60.0; }
constexpr double s_to_ms(double s){ return s * 1000.0; }

// ---------- 编译期验证 ----------
static_assert(km_to_m(1.0)   == 1000.0, "1 km 应为 1000 m");
static_assert(km_to_m(2.5)   == 2500.0, "2.5 km 应为 2500 m");
static_assert(m_to_km(500.0) == 0.5,    "500 m 应为 0.5 km");
static_assert(cm_to_m(250.0) == 2.5,    "250 cm 应为 2.5 m");
static_assert(inch_to_m(1.0) == 0.0254, "1 inch 应为 0.0254 m");

static_assert(h_to_s(1.0)   == 3600.0, "1 h 应为 3600 s");
static_assert(min_to_s(90.0)== 5400.0, "90 min 应为 5400 s");
static_assert(s_to_ms(1.5)  == 1500.0, "1.5 s 应为 1500 ms");

// 组合换算:km/h -> m/s,同样可在编译期完成
constexpr double kmh_to_ms(double v) { return km_to_m(v) / h_to_s(1.0); }
static_assert(kmh_to_ms(36.0) == 10.0, "36 km/h 应为 10 m/s");

int main() {
    // 运行期同样复用这些函数
    double d_km = 3.2;
    double d_m  = km_to_m(d_km);
    assert(d_m == 3200.0);
    return 0;
}

编译期断言的三个易错点

  • 实参不是常量表达式:static_assert(km_to_m(x) == 1000.0) 中若 x 是普通变量,编译报错"不是常量表达式"。断言里必须直接写字面量或 constexpr 变量。
  • 浮点相等比较:本例系数都是二进制可精确表示的整数倍(1000、3600、0.5、2.5、0.0254 需注意),0.0254 无法精确表示,但两侧用同一字面量参与运算,结果位模式一致,== 成立。若换成 0.1 这类系数并做多次乘除,应改用容差比较,例如 constexpr bool near(double a, double b) { return (a > b ? a - b : b - a) < 1e-9; }。
  • 整数除法截断:若把参数写成 int,cm_to_m(250) 会得到 2 而非 2.5。单位换算统一用 double,或在函数内显式转换。
把 static_assert 当作"编译期的单元测试":每新增一个换算函数,就补一条正向和一条反向断言。这样系数抄错、量纲写反都会在编译阶段暴露,而不是等到运行期数值异常。
C++14 起 constexpr 函数才允许局部变量、if 和循环;若项目仍停留在 C++11,函数体只能是一条 return 语句,组合换算需要拆成多个单表达式函数。

验证与扩展

编译命令:g++ -std=c++17 -Wall -Wextra -pedantic main.cpp -o units。若所有 static_assert 通过,编译无输出;故意把 km_to_m 的系数改成 100,会立刻看到类似 error: static assertion failed: 1 km 应为 1000 m 的提示,这就是编译期验证的价值。

扩展方向:把基准单位换成强类型(如 struct Meters { double v; };),让 km_to_m 返回 Meters 而非裸 double,可在编译期阻止"米和秒相加"这类量纲错误,与 constexpr 结合后仍保持零运行期开销。