Logo成贤计协指南

const 与 constexpr

只读与常量

在 C++ 中,我们经常会遇到“常量”的概念。但随着标准的发展,const 和 C++11 引入的 constexpr 之间有了非常微妙且重要的区别。

const

const 的含义是“只读”。它告诉编译器:这个变量一旦初始化,就不能再被修改。但是,const 并不一定代表它是“编译期已知”的。

const int a = 5; // ✅ 只读,而且可作为常量表达式使用

int x;
std::cin >> x;
const int y = x; // ✅ 正确:y 是只读的,但它的值直到运行时才知道

因此,实际上 const 并不完全是我们之前所认为的常量。他的用法实际上和 Rust、Kotlin 等其他语言中的不可变变量类似,可以用于确保定义的变量在后续中不被改变。实际上在日常的业务开发中,我们使用 const 的核心目的就是为了防御性编程:防止自己或同事在后续的代码中手滑,将本不应该变动的值改变。

constexpr

constexpr (Constant Expression) 是 C++11 引入的,它的含义是“常量表达式”。它告诉编译器:这个值在编译期间就必须计算出来

constexpr int x = 10; // ✅ 正确:10 是编译期已知的
// constexpr int y = get_input(); // ❌ 错误:输入值直到运行时才知道

为什么要用 constexpr?

  1. 性能极佳:计算在编译程序时就完成了,程序运行时直接取结果,零开销。
  2. 安全性:某些地方(如数组长度、模板参数)强制要求必须是编译期常量。
  3. 代码优化:编译器看到 constexpr 结果,可以进行更大胆的优化。

constexpr 函数

constexpr 不仅可以修饰变量,还可以修饰函数。这意味着如果传入的参数是编译期常量,那么整个函数的调用都会在编译阶段被替换为结果。

constexpr int square(int x) {
    return x * x;
} // C++ 11 和 C++ 14 之后的 constexpr 函数有些许差异,可以自行查阅文档。

int main() {
    constexpr int res = square(5); // 编译后直接变成 int res = 25;
}

Tips

constexpr 函数也可以在运行时当作普通函数使用。如果传入的是运行时的变量,它就会像普通函数一样工作。

const 与 constexpr 的选择建议

如果你想定义一个编译期就能确定的常量(如 π 的值、固定的配置),优先使用 constexpr

Tips

如果你接触过 C 或者老的 C++ 教程,可能你会知道使用宏来定义常量,比如 #define PI 3.14。在现代 C++ 中,我们不推荐使用该方法。宏没有办法使用 C++ 的命名空间,是全局污染的。如果你引入了一个第三方物理库,里面也定义了 #define PI,你的代码可能会出现莫名其妙的问题。

(宏本身的全局污染性会导致很多问题,比如臭名昭著的 windows.h 中的 minmax 宏。他会直接导致你在使用 C++ std 命名空间的 std::min std::max 函数出现问题。因为他会把 std::max(a,b) 给替换为 std::(((a) > (b)) ? (a) : (b))(a, b)

因此,在现代 C++ 中,在可以的情况下,尽量使用 constexpr 来代替。

而如果你只是想保护一个变量不被修改,但它的值可能需要通过计算或输入获得,使用 const。一个常见实践是,如果变量在初始化之后不需要再改变,就优先把它声明为 const。只有在确实需要修改时,再去掉 const。这样可以减少意外赋值、状态漂移等问题。例如:

int a_number = 1;

if (a_number = 10) { // ❌ 这里的 = 是赋值,不是 ==。虽然部分编译器可能会有警告,但是基本不会报错。同时,这个 if 将永远成立。
    // ...
}

而如果你使用 const int a_number = 1,由于 a_number 不可变,所以编译器会直接报错,更容易尽早暴露问题。实际上,程序中很大部分的变量在初始化之后不会被重新赋值。不可变性可以减少因为改变这些值而导致的问题。也因此,很多现代语言会把定义变量的默认行为设为不可变变量。

Last updated on

On this page