编译期多态及编译期计算¶
1.编译期多态
1.1CRTP
#include <iostream>
// 基类模板
template <typename Derived>
class Base {
public:
// 调用派生类的具体实现
void doSomething() { static_cast<Derived*>(this)->doSomethingImpl(); }
};
// 派生类
class Derived1 : public Base<Derived1> {
public:
// 派生类的具体实现
void doSomethingImpl() {
std::cout << "Derived1 is doing something." << std::endl;
}
};
// 另一个派生类
class Derived2 : public Base<Derived2> {
public:
// 派生类的具体实现
void doSomethingImpl() {
std::cout << "Derived2 is doing something." << std::endl;
}
};
// 辅助函数,接受基类引用并调用其doSomething方法
template <typename T>
void performAction(Base<T>& obj) {
obj.doSomething();
}
int main() {
Derived1 d1;
Derived2 d2;
performAction(d1);
performAction(d2);
return 0;
}
// 一般用于高频调用的场景,像处理外部数据源的接口、抽象功能而复用代码。
1.2concepts类型多态
template <typename T>
concept GraphBuilder =
HasBuildGraph<T, typename T::DistanceSpaceTypeAlias::DataTypeAlias,
typename T::DistanceSpaceTypeAlias::IDTypeAlias>;
// 构建器1:使用float和uint32_t
struct BuilderA {
struct DistanceSpace {
using DataTypeAlias = float;
using IDTypeAlias = uint32_t;
};
using DistanceSpaceTypeAlias = DistanceSpace;
auto build_graph() -> std::unique_ptr<Graph<float, uint32_t>> {
return std::make_unique<Graph<float, uint32_t>>(...);
}
};
// 构建器2:使用uint8_t和uint64_t
struct BuilderB {
struct DistanceSpace {
using DataTypeAlias = uint8_t;
using IDTypeAlias = uint64_t;
};
using DistanceSpaceTypeAlias = DistanceSpace;
auto build_graph() -> std::unique_ptr<Graph<uint8_t, uint64_t>> {
return std::make_unique<Graph<uint8_t, uint64_t>>(...);
}
};
// 编译时多态函数
template <GraphBuilder Builder>
void process_graph(Builder& builder) {
auto graph = builder.build_graph();
// 编译器为BuilderA和BuilderB生成不同实现
}
// 使用
BuilderA a;
BuilderB b;
process_graph(a); // 实例化process_graph<BuilderA>
process_graph(b); // 实例化process_graph<BuilderB>
1.3模板特化
template <OrderType Type>
struct OrderProcessor;
template <>
struct OrderProcessor<OrderType::BUY> {
static void process(const Order& order) { /* 买单逻辑 */ }
};
template <>
struct OrderProcessor<OrderType::SELL> {
static void process(const Order& order) { /* 卖单逻辑 */ }
};
// 编译期分发
template <OrderType Type>
void processOrder(const Order& order) {
OrderProcessor<Type>::process(order);
}
1.4 if constexpr 编译时多态
template <typename Animal>
auto speak(const Animal& a) {
if constexpr (std::is_same_v<Animal, Bear>) {
a.roar();
} else if constexpr (std::is_same_v<Animal, Duck>) {
a.quack();
} else {
static_assert(false);
} // Trig compilation error
}
1.5 函数/操作符重载是最简单的编译期多态方法
2.编译期计算
const: 一次赋值的常量变量
1. 核心特性与价值
- 不可修改性:C++ 编译器会强制阻止对
const变量的后续修改,若代码中出现const int x = 5; x = 10;这类语句,会直接编译报错,避免意外修改导致的 Bug; - 初始化灵活性:与
#define宏(必须编译期确定)或constexpr(优先编译期计算)不同,const变量的初始化器可来自运行时函数调用、配置读取等动态场景,例如:
// 示例1:运行时函数返回值初始化 const 变量
const int scale_factor =
get_config("scale"); // get_config 读取外部配置,运行时确定值
// 示例2:静态数组初始化(初始值为编译期常量,也可适配运行时动态数组,但需注意数组大小需编译期确定)
const int primes[] = {2, 3, 5, 7,
11, 13, 17}; // 数组元素不可修改,数组大小编译期确定
- 编译器优化基础:尽管
const变量的初始化可能依赖运行时,但编译器会利用其“不可修改”的特性,进行常量传播、冗余代码删除等优化。例如,若const int max_len = 100;后续仅用于数组大小或条件判断,编译器可直接替换为常量值,避免运行时变量访问开销。
2. 与“符号常量”的区别
const 变量与 #define 符号常量的差异,二者虽都用于表示“不变的值”,但在类型安全性与灵活性上存在显著区别:
| 特性 | const 变量 | #define 符号常量 |
|---|---|---|
| 类型信息 | 有明确类型(如 const int),编译器可进行类型检查 |
无类型,本质是文本替换,可能引发类型不匹配问题 |
| 作用域 | 遵循 C++ 作用域规则(如局部 const 仅在函数内有效) |
全局有效,从定义处到文件结束,易引发命名冲突 |
| 初始化方式 | 支持运行时初始化(如函数返回值) | 仅支持编译期常量初始化 |
| 内存占用 | 可能占用内存(若取地址或全局可见),但编译器可优化为立即数 | 不占用内存,仅在预处理阶段替换为常量值 |
优先使用 const 变量而非 #define——例如表示配置参数、固定数组等场景,const 的类型安全性可避免宏替换导致的隐蔽错误(如 #define PI 3.14 被误用作整数)。
const 的其他核心应用场景
1. 函数参数中的 const:避免意外修改与优化传参
// 示例:const 引用传递,避免对象拷贝与意外修改
void print_vector(const std::vector<int>& vec) {
// 函数内无法修改 vec 的元素,同时避免值传递的拷贝开销
for (int val : vec) std::cout << val << " ";
}
2. const 对象与 const 成员函数:只读对象的安全访问
class Complex {
private:
double real, imag;
public:
// const 成员函数:不修改对象数据,仅读取
double get_real() const { return real; }
// 非 const 成员函数:修改对象数据
void set_real(double r) { real = r; }
};
const Complex cfactor(3.14, 1.0); // const 对象
cfactor.get_real(); // 合法:调用 const 成员函数
cfactor.set_real(2.0); // 编译报错:const 对象无法调用非 const 成员函数
3. 非成员函数的 const 限制
非成员函数(如全局函数、友元函数)不能被 const 修饰——const 对函数的修饰仅适用于类成员函数(限定函数对对象状态的修改权限)。若需限制非成员函数对参数的修改,需在参数层面使用 const,例如:
// 非成员函数:通过参数 const 限制修改,而非函数本身 const
void process_data(const int* data, int len) {
// 无法修改 data 指向的内容,仅能读取
}
const 与其他编译期技术的关联
1. const vs constexpr:编译期计算的优先级差异
- const:仅保证“初始化后不可修改”,不强制编译期计算。
- constexpr:是“编译器优化提示”,强制要求“若初始化器可编译期计算,则必须在编译期完成”。
// constexpr 变量:优先编译期计算,若初始化器无法编译期确定则报错
constexpr float PI = 3.14f; // 编译期确定,运行时无任何开销
constexpr int twice(int x) {
return x + x;
} // constexpr 函数,参数为常量时编译期计算
constexpr int four = twice(2); // 编译期计算为 4,运行时直接使用常量值
// 错误示例:constexpr 无法使用运行时初始化器
constexpr int scale =
get_config("scale"); // 编译报错:get_config 是运行时函数,无法编译期确定
// constexpr函数有一些限制;不允许执行以下操作:
// 处理本地静态变量
// 处理thread_local变量
// 调用任何函数,本身不是constexpr函数
2.const vs constinit:静态生命周期的编译期初始化
constinit 是 C++20 引入的特性,是“consteval 与静态变量的混合体”——其核心是保证变量在编译期初始化,且具有静态生命周期(如全局变量、静态局部变量),但初始化器无需是“严格的编译期常量表达式”,可适配“部分路径为编译期计算”的函数调用。
| 特性 | const 变量 | constinit 变量 |
|---|---|---|
| 初始化时机 | 可运行时初始化(如函数调用) | 强制编译期初始化 |
| 生命周期 | 遵循正常变量生命周期(如局部变量在栈上) | 必须是静态生命周期(全局、静态局部变量) |
| 可修改性 | 初始化后不可修改 | 初始化后不可修改(隐含 const 特性) |
| 适用场景 | 局部/全局的一次赋值变量 | 全局静态变量、静态局部变量的编译期初始化 |
若函数 somefunc 存在“编译期常量路径”与“运行时路径”,constinit 可强制其在编译期选择常量路径初始化静态变量,而 const 无法保证编译期初始化:
// 函数:部分路径返回编译期常量,部分路径返回运行时随机值
int somefunc() {
#ifdef COMPILE_TIME
return 27; // 编译期可确定
#else
return some_random_number(); // 运行时确定
#endif
}
// constinit 变量:强制编译期初始化,选择 27 作为初始值
constinit int s_myconst = somefunc(); // 编译期完成初始化,运行时无开销
// const 变量:若 somefunc 调用运行时路径,会在运行时初始化,无法保证编译期完成
const int s_myconst_const = somefunc();
3. const 与模板:编译期类型适配与优化
书模板(Templates)是编译期优化的“强大工具”,而 const 变量可作为模板的“非类型参数”,实现“编译期确定模板实例化参数”,进一步提升代码针对性与效率。
例如,在 AI 引擎的向量计算场景中,可通过 const 常量指定向量维度,模板会为每个维度生成专属代码:
// 模板类:以 const 常量作为非类型参数,指定向量维度
template <const int Dim> // Dim 为编译期 const 常量
class Vector {
private:
float data[Dim]; // 数组大小由 Dim 确定,编译期分配
public:
// 向量点积计算:编译器可针对 Dim 优化,如 Dim=4 时手动展开循环
float dot(const Vector<Dim>& other) const {
float sum = 0.0f;
for (int i = 0; i < Dim; i++) {
sum += data[i] * other.data[i];
}
return sum;
}
};
// 实例化模板:Dim 为 const 常量 4,编译期生成 Vector<4> 的专属代码
const int vec_dim = 4;
Vector<vec_dim> v1, v2;
float result = v1.dot(v2); // 编译器针对 4 维向量优化,无运行时维度判断开销
const 常量作为模板参数,将“维度判断”从运行时转移到编译期,避免动态分支(如 if (dim == 4) { ... }),同时让编译器为每个维度生成最优代码。
4.consteval:函数的编译期强制执行与计算
consteval 修饰的函数称为立即函数,强制函数必须在编译期执行,返回值一定是编译期常量:
- 若函数无法在编译期计算(如依赖运行时数据),会直接编译报错
- 其返回值可直接作为模板非类型参数,无需额外
const修饰 - 确保计算在编译期完成,完全消除运行时开销
// 合法:vec_dim 是编译期可知的 const 常量
const int vec_dim = 4;
Vector<vec_dim> v1; // 正确
// 合法:consteval 函数返回编译期常量
consteval int get_dim() { return 4; }
Vector<get_dim()> v2; // 正确(编译期计算)
// 不合法:运行时初始化的 const 变量不能作为模板参数
int n = 4;
const int runtime_dim = n; // runtime_dim 是运行时常量
Vector<runtime_dim> v3; // 编译错误!
查找表(LUT)与预计算
查找表(LUT)是存储“预计算结果”的数组结构,预计算则是“提前完成计算并存储结果”的优化策略。
本质:以“空间换时间”——通过预计算将复杂运算(如非线性函数、三角函数)的结果存储在数组中,运行时通过索引直接获取结果,避免重复计算;
预计算可在“编译前、编译时、程序启动时、运行时首次使用时”四个节点完成,不同节点对应不同效率与灵活性:
| 预计算节点 | 实现方式 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 编译前(离线) | 用独立程序生成二进制数据文件或C++源代码 | 零运行时开销,数据直接链接进可执行文件 | 灵活性低,需重新生成文件以更新数据 | 静态配置参数 |
| 编译时 | constexpr函数、静态数组初始化 |
无运行时初始化开销,数据存于只读段 | 仅支持编译期可计算的常量表达式 | 小范围数学函数 |
| 程序启动时 | 初始化函数填充LUT数组 | 支持动态参数(如配置文件读取的参数) | 增加启动延迟,占用运行时内存 | 动态配置的稀疏掩码 |
| 运行时首次使用时 | 延迟计算(Lazy Evaluation),首次使用时填充 | 避免未使用数据的预计算与内存占用 | 首次调用有计算延迟,需加线程安全控制 | 低频使用的备用函数、动态输入范围 |
C++26 Expansion Statements:编译期循环(未来支持)
Expansion Statements的核心语法形式为template for,编译器直接将循环体代码复制N次,不生成跳跃指令(jmp) 。
Expansion Statements采用类似C++运行时循环的语法,但添加了template关键字以表明其编译期特性。简单的编译期循环可以写成:
0..4表示一个编译期生成的整数序列,循环变量i在每次迭代中依次取0到4的值。
Expansion Statements的工作原理基于C++模板元编程的两阶段特性。在编译期,编译器会实例化模板并展开循环体,将循环转换为一系列静态代码。这种展开过程在编译器内部进行,不会产生任何运行时开销,同时避免了传统递归模板中可能出现的实例化膨胀问题。