C++有如下四种常量:
| 关键字 | 核心本质 | 求值与初始化 | 说明 |
|---|---|---|---|
const | 只读 (Read-Only) 接口契约。 | 运行时或编译期求值; 在运行时或静态初始化阶段完成初始化。 | 典型用于限制入参修改、运行时只读配置。 |
constexpr | 编译期常量 (Constant Expression)。 | 强制编译期求值; 在编译期或静态初始化阶段完成初始化。 | 作为数组长度、模板参数、元编程的基石。 |
consteval | 立即函数 (Immediate Function)。 | 严格强制编译期求值; 在编译期完成初始化。 | 仅能修饰函数。典型用于硬件配置校验、编译期字符串解析。 |
constinit | 静态初始化断言 (Static Init Assertion)。 | 编译期求值(初始化表达式);强制在静态初始化阶段完成初始化。 | 约束为静态或线程局部存储。用于彻底解决 SIOF 问题的全局非常量变量。 |
const:只读契约与指针/引用语义const 在 C++ 中代表 Read-only(只读视图)而非 Immutable(不可变内存)。若对象分配在栈或堆上,通过 const_cast 强转修改其内容,在语法上可行,但如果对象本身处于只读数据段(.rodata),强转修改会导致 Segment Fault (OOB/Access Violation)。const 变量若未被取地址且编译器完成了常量折叠,可能不占用符号表与物理内存。在系统编程中,涉及多级指针、无符号类型转换或硬件 DMA 缓冲区时,必须精准区分:
// 生产级内存映射/指针控制示例
using byte = unsigned char;void process_buffer(const byte* const src, byte* const dest, size_t size) {
// src: 底层 const + 顶层 const -> 指针不能动,指针指向的数据也不能动 (输入源)
// dest: 顶层 const -> 指针不能动,但指向的数据可写 (输出缓冲区) // 输入校验(防御性设计,不允许静默失败)
if (!src || !dest || size == 0) {
throw std::invalid_argument("Invalid buffer pointers or size.");
} for (size_t i = 0; i < size; ++i) {
dest[i] = src[i];
}
}
constexpr:编译期计算计算右移 (Shift-Left on Computation):通过将计算移至编译期,不仅实现了零运行时开销 (Zero-overhead),还能将原本需要运行时进行的错误捕获提前到编译期(通过编译期 throw 触发编译失败)。
C++14/20 的演进契机:
constexpr 函数内部使用局部变量、条件分支、循环。constexpr std::vector 和 std::string(可在编译期进行动态内存分配,但内存必须在编译期结束前释放)。示例:编译期 CRC32 校验
#include <string_view>
#include <cstdint>
#include <array>
#include <stdexcept>class Crc32 {
private:
// C++14 兼容的编译期查找表生成
static constexpr std::array<uint32_t, 256> make_table() noexcept {
std::array<uint32_t, 256> table{};
for (uint32_t i = 0; i < 256; ++i) {
uint32_t ch = i;
for (size_t j = 0; j < 8; ++j) {
ch = (ch & 1) ? (0xEDB88320 ^ (ch >> 1)) : (ch >> 1);
}
table[i] = ch;
}
return table;
}// C++17 inline constexpr 允许在类内直接定义并初始化静态成员
static inline constexpr auto table = make_table();public:
// constexpr 函数:既可在编译期计算,也可在运行时计算
static constexpr uint32_t calculate(std::string_view str) noexcept {
uint32_t crc = 0xFFFFFFFF;
for (char c : str) {
crc = table[(crc ^ static_cast<uint8_t>(c)) & 0xFF] ^ (crc >> 8);
}
return ~crc;
}
};// 生产验证
void daemon_init() {
// 编译期求值:完全消除了运行时的字面量哈希开销
constexpr uint32_t kConfigHash = Crc32::calculate("SYS_CONFIG_V1");
static_assert(kConfigHash == 0x7E303DCE, "Compile-time CRC32 verification failed.");
}
consteval:强不可逆的立即函数constexpr 函数具有双重性(参数为非常量时降级为运行时执行)。如果系统架构要求某项高耗时计算绝对不允许流向运行时(例如:大型密码学 S 盒生成、固件校验和),必须使用 consteval。示例:强制编译期哈希避免运行时解析
#include <string_view>
#include <stdexcept>// consteval 确保运行时没有任何字符串处理
consteval uint64_t fnv1a_hash(std::string_view str) noexcept {
uint64_t hash = 0xcbf29ce484222325;
for (char c : str) {
hash ^= static_cast<uint64_t>(c);
hash *= 0x00000100000001B3;
}
return hash;
}void route_message(uint64_t msg_id) {
// 如果不小心传入运行时变量,编译器会立刻熔断报错
switch (msg_id) {
case fnv1a_hash("LOG_EVENT"): // 100% 编译期常量
break;
case fnv1a_hash("NETWORK_ERR"):
break;
}
}
constinit:终结静态初始化顺序陷阱静态初始化顺序陷阱(Static Initialization Order Fiasco,SIOF ):不同编译单元(.cpp)中的全局静态变量,其初始化顺序在标准中是未定义的。若 A.cpp 的全局变量依赖 B.cpp 的全局变量,极易引发运行时未定义行为或崩溃。
底层机理:C++ 静态存储期变量的初始化分为两个阶段:静态初始化(Static Initialization)与动态初始化(Dynamic Initialization)。
_start 入口之后、调用 .init_array 节槽位之前,直接将编译期字面量写入特定数据段);constinit 强制断言修饰的变量必须在“静态初始化”阶段完成赋值(即要求初始化器必须是编译期常量表达式),彻底阻断了其进入动态初始化阶段的可能,从而根治 SIOF。注意:constinit 保证的是“初始化安全”,它修饰的变量不是常量,后续运行中可以随意修改。它与 constexpr 的本质区别在于:
constexpr 隐式包含 const 属性且要求变量本身不可变constinit 仅规范初始化时机,不改变变量的可变性(Mutability)。
示例:可安全访问的全局单例/上下文
#include <cstdint>struct SystemMetrics {
uint64_t total_requests;
double alpha_weight;
// 提供 constexpr 构造函数以支持常量初始化
constexpr SystemMetrics(uint64_t req, double weight) noexcept
: total_requests(req), alpha_weight(weight) {}
};// 使用 constinit 确保该全局上下文在程序加载的 Phase-0 阶段即静态初始化完成
// 它免除了 SIOF 烦恼,且它不是 const,运行时工作线程可以自由对其进行原子或并发修改
inline constinit SystemMetrics g_system_metrics{0, 0.85};
都为编译期计算的基石
基本目标:都旨在将计算任务从“运行时”向“编译期”右移(Shift-Left),从而实现零运行时开销(Zero-overhead),并允许将计算结果用于需要常量表达式的上下文(如模板参数、static_assert、数组长度)。
语法限制:其内部代码都必须符合 C++ 标准对常量表达式函数(constexpr function)的限制。如:
constexpr 函数goto 语句(C++14 后放宽了循环和局部变量限制,C++20 后支持了编译期动态内存分配但必须在编译期结束前释放)。隐式内联:修饰函数时,两者都会隐式地将函数标记为 inline。
两者间的差异
| 维度 | constexpr (C++11 引入) | consteval (C++20 引入) |
|---|---|---|
| 核心本质 | 条件性/双重性常量函数 | 立即函数 (Immediate Function) |
| 求值时机 | 编译期或运行时。如果参数是编译期常量,且其结果被用于常量上下文,则在编译期求值;否则会退化为普通的运行时函数。 | 严格强制编译期求值。每一次调用都必须在编译期完成,绝不允许流向运行时。 |
| 修饰对象 | 既可以修饰函数,也可以修饰变量(表示该变量是编译期常量且不可变)。 | 只能修饰函数,不能修饰变量。 |
| 不满足编译期条件的行为 | 传入运行时变量时,正常降级并在运行时执行。 | 传入运行时变量,或者无法在编译期完成求值时,编译器直接熔断报错。 |
mutable 的权衡const(如 int get_data() const;)向调用者承诺不改变对象的逻辑状态。const 成员函数内部加锁或读写缓存。这时必须引入 mutable 关键字修饰成员锁或缓存组件,从而在物理内存可变的前提下,捍卫逻辑层面的只读契约。#include <shared_mutex>
#include <string>
#include <stdexcept>class ThreadSafeConfig {
private:
std::string config_path_;
mutable std::shared_mutex rw_mutex_; // 为了在 const 函数中控制并发,必须为 mutable
mutable std::string cached_data_ {};
mutable bool is_dirty_{true};public:
explicit ThreadSafeConfig(std::string path) : config_path_(std::move(path)) {
if (config_path_.empty()) {
throw std::invalid_argument("Config path cannot be empty.");
}
} // 虽然是 const 函数,但通过 mutable 配合 shared_mutex 实现了高性能并发读与延迟加载
std::string get_config() const {
// 1. 先施加读锁(共享锁),允许多线程并发读取缓存
{
std::shared_lock<std::shared_mutex> read_lock(rw_mutex_);
if (!is_dirty_) {
return cached_data_;
}
} // 2. 缓存失效,升级为写锁(排他锁)刷新缓存
std::unique_lock<std::shared_mutex> write_lock(rw_mutex_);
// 双检锁模式,防止并发写锁排队时重复加载
if (is_dirty_) {
// 模拟高昂的 IO 物理读取
cached_data_ = "Loaded_Data_From_" + config_path_;
is_dirty_ = false;
}
return cached_data_;
}
};
入参:优先采用 const T& 避免大对象拷贝;对于简单标量(如 int, char, float、std::string_view、std::span),直接值传递性能更高,无须加 const。
出参:
const T。这会强制破坏 C++11 的移动语义 (Move Semantics),阻碍编译器进行 RVO (返回值优化)。const T&。#define MATCH_SIZE 1024。宏不受命名空间约束,无类型保护,且在符号调试(GDB/LLDB)中完全隐形。constexpr 优先:凡是编译期能确定的配置、大小、字面量变换,一律声明为 constexpr。namespace 内,避免污染全局域。常量声明示例
#include <string_view>
#include <cstddef>
#include <cstdint>namespace sys::config {
// 基础标量常量
inline constexpr std::string_view kEngineVersion = "v2.4.1";
inline constexpr size_t kMaxNetworkPacketSize = 65536;
inline constexpr double kEpsilon = 1e-6;
// 复杂配置结构体编译期初始化
struct DeviceProperty {
uint32_t id;
size_t alignment;
};
inline constexpr DeviceProperty kDefaultDmaProp{0x0A, 4096};
}
const 代表性能优化
const 会被折叠外,运行时的 const T& 往往因为指针别名问题 (Pointer Aliasing) 限制了编译器的寄存器优化。当编译器在一个作用域内同时看到 const T* 和 T* 时,由于无法确信非 const 指针是否在暗中修改了同一块物理内存(Strict Aliasing),它不得不放弃寄存器缓存优化,在每次读取时都生成一条防御性的内存重载指令(Memory Reload)。constexpr 函数只能在编译期跑
constexpr 函数是非常温和的。除非你在要求强制常量上下文(如 static_assert、数组定义、constexpr 变量声明)中调用它,否则传入运行时参数时,它会自动退化为普通的运行时函数。通过 const_cast 修改 const 对象是安全的
const(处于只读存储区 ),对其进行 const_cast 并写值属于未定义行为 (Undefined Behaviour),在底层通常会引发段错误。只有当原对象本身是非 const 的,只是在传递过程中被 const& 引用包裹时,强转修改在物理上才是安全的(但极度不推荐这种破坏设计契约的行为)。