概念
constexpt关键字是在c++11版本引入的重要特性,它的主要作用是在编译时运算,提供编译时常量。
基本用法
常量表达式
// 1. 基本常量
constexpr int value = 42; // 编译时常量
constexpr double pi = 3.14159; // 编译时常量
constexpr int square(int x) { return x * x; } // 编译时函数
// 2. 在编译时计算
constexpr int result = square(5); // 编译时计算结果为25
函数声明
注意在c++11版本,仅能包含一条return语句
class Math {
public:
// constexpr函数
constexpr int add(int a, int b) const {
return a + b;
}
// constexpr构造函数
constexpr Math() : value(0) {}
private:
int value;
};
constexpt与const的区别
// const:运行时常量
const int var1 = std::rand(); // 运行时确定值
// constexpr:编译时常量
constexpr int var2 = 42; // 必须在编译时确定值
void example() {
const int arr_size1 = rand(); // OK:运行时确定
int arr1[arr_size1]; // 错误:数组大小必须是常量表达式
constexpr int arr_size2 = 10; // 编译时确定
int arr2[arr_size2]; // OK:可以用作数组大小
}
不同版本constexpr特性
c++11版本
// 1. 函数体只能包含一条 return 语句
constexpr int square(int x) {
return x * x; // 只能有这一条语句
}
// 2. constexpr 构造函数的限制
class Point {
int x, y;
public:
constexpr Point(int ix, int iy) : x(ix), y(iy) {} // 只能包含初始化列表
constexpr int getX() const { return x; }
constexpr int getY() const { return y; }
};
// 3. 递归是允许的
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
c++14版本
允许多条语句
constexpr int factorial(int n) {
int result = 1;
for (int i = 1; i <= n; ++i) { // 允许循环
result *= i;
}
return result;
}
允许声明变量
constexpr Point movePoint(Point p, int dx, int dy) {
int new_x = p.getX() + dx; // 允许局部变量
int new_y = p.getY() + dy;
return Point(new_x, new_y);
}
允许if语句
constexpr int abs(int x) {
if (x < 0) { // 允许if语句
return -x;
}
return x;
}
c++17版本
constexpr if
template <typename T>
constexpr auto process(T value) {
if constexpr (std::is_integral_v<T>) {
return value * 2;
} else if constexpr (std::is_floating_point_v<T>) {
return value + 0.5;
} else {
return value;
}
}
constexpr lambda
// constexpr lambda表达式
auto square = [](int x) constexpr { return x * x; };
constexpr int result = square(4); // 编译期计算
// 泛型lambda
auto generic_square = [](auto x) constexpr { return x * x; };
constexpr int i_result = generic_square(4); // int
constexpr double d_result = generic_square(4.0); // double
c++20版本
constexpr 虚函数
class Base {
public:
constexpr virtual int getValue() const { return 42; }
};
class Derived : public Base {
public:
constexpr int getValue() const override { return 84; }
};
constexpr int test() {
Derived d;
const Base& b = d;
return b.getValue(); // 编译期多态
}
constexpr std::string和std::vector
constexpr std::string str = "Hello";
constexpr bool starts_with_h = str.starts_with('H');
constexpr std::vector<int> vec = {1, 2, 3};
constexpr int first = vec[0];
c++23版本
constexpr typeinfo
constexpr auto type_name = typeid(int).name();
更多的标准库支持
std::optional
std::variant
std::function
更多算法和容器操作
constexpr原理分析
通过上面的概念了解了constexpr是显式的指明函数或者变量在编译期运算,那么他的原理该怎么理解呢?比如对一个std::string而言使用constexpr与否到底有什么区别呢?请看下面的讲解。
std::string 通常的内存布局包括:
class string {
struct {
char* data; // 指向实际字符数据的指针
size_t size; // 字符串长度
size_t capacity; // 分配的内存容量
};
// 可能还有小字符串优化(SSO)的缓冲区
};
使用constexpr与否的区别:
void example() {
// 运行时版本
std::string s1 = "Hello";
// 执行过程:
// 1. 在栈上分配 std::string 对象本身的空间
// 2. 在堆上分配存储字符的空间
// 3. 将 "Hello" 从只读数据段复制到堆内存
// 4. 函数结束时析构 string,释放堆内存
// 编译期版本
constexpr std::string s2 = "Hello";
// 编译器优化后:
// 1. 在栈上分配 std::string 对象的空间
// 2. string 的数据直接指向只读数据段的 "Hello"
// 3. 不需要堆内存分配和复制
}
编译器对 constexpr std::string 可以做特殊优化:
// 编译器可能将这段代码
constexpr std::string s2 = "Hello";
// 优化成类似这样的实现
struct {
const char* data = "Hello"; // 直接指向只读数据段
size_t size = 5; // 编译期确定的大小
size_t capacity = 5; // 编译期确定的容量
} s2;
注意事项和限制
基本注意事项
class Restrictions {
public:
// 1. constexpr函数必须有返回值
constexpr void invalid() {} // 错误:void返回类型
// 2. 不能包含静态或线程局部数据
constexpr int wrong() {
static int x = 0; // 错误
return x;
}
// 3. 不能有副作用,比如调用只有在运行阶段才能进行的操作
constexpr int no_side_effects() {
std::cout << "Hello"; // 错误:不允许I/O操作
return 42;
}
};
注意事项二
- constexpr关键字修饰的函数可以在编译阶段调用,也可以在运行时,如果强调在编译时调用一定采用constexpr修饰,否则都是在默认阶段调用
class MyClass {
int value;
public:
constexpr MyClass(int v) : value(v) {}
constexpr int getValue() const { return value; }
constexpr MyClass add(const MyClass& other) const {
return MyClass(value + other.getValue());
}
};
void mixed_example() {
// 1. 编译期计算
constexpr MyClass obj1(10);
constexpr MyClass obj2(20);
constexpr int sum = obj1.add(obj2).getValue(); // 编译期计算得30
// 2. 运行时计算
MyClass runtime_obj1(15);
MyClass runtime_obj2(25);
int runtime_sum = runtime_obj1.add(runtime_obj2).getValue(); // 运行时计算
// 3. 混合使用
constexpr MyClass const_obj(100);
MyClass runtime_obj(50);
// 以下都是运行时计算
int val1 = const_obj.getValue(); // 虽然对象是constexpr,但这里是运行时调用
int val2 = runtime_obj.getValue(); // 运行时调用
}