constexpr 限制放宽

1. constexpr 概述

constexpr(常量表达式)是 C++11 引入的关键字,用于指定值或函数可以在编译时计算,是编译时计算的核心特性。其设计目标主要有以下几点:

  • 编译时计算:将运行时计算转移到编译时
  • 类型安全:编译时进行类型检查
  • 性能优化:消除运行时开销
  • 泛型编程:增强模板元编程能力
1
2
3
4
5
6
// 编译时常量
constexpr int size = 100; // ✅ 编译时确定
// 编译时函数
constexpr int square(int x) { return x * x; }
// 编译时使用
int array[square(5)]; // ✅ 数组大小在编译时计算:25

关于constexpr的使用和const是类似的,下面的表格中将二者的特性进行是对比:

特性 const constexpr
主要含义 只读,不可修改 编译时常量
初始化时机 运行时或编译时 必须编译时
能否修改 初始化后不可修改 初始化后不可修改
作用域 类型修饰符 类型修饰符 + 函数修饰符
C++版本 C++98 C++11+
  1. const 初始化时机不一定是编译时

    1
    const T x = expr;
    • 如果expr常量表达式,则x是编译时常量
    • 否则,x是运行时常量(只读变量)
  2. constexpr 初始化时机一定是编译时

    1
    constexpr T x = expr;  // expr必须是常量表达式
    • expr必须在编译时可求值
    • 否则编译失败
  3. constexpr 隐含 const 属性

    1
    2
    constexpr int a = 10;
    a = 20; // ❌ 编译错误:constexpr变量也是const

最后给大家列举一些需要编译时常量的场景:

  1. 数组大小int array[N];

  2. 模板非类型参数template<int N>

  3. case标签case VALUE:

  4. 位域宽度(指定一个整型成员使用多少个比特(bit)来存储数据)unsigned int x : WIDTH;

  5. 枚举值初始化enum E { val = N };

  6. 对齐说明符alignas(N) -

    alignas(N) 是C++11引入的对齐说明符,用于显式指定变量、类、结构体或数组在内存中的对齐方式。语法格式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 1. 直接对齐数值
    alignas(alignment) type variable;

    // 2. 使用类型作为对齐参考
    alignas(type) type variable;

    // 3. 多种写法
    alignas(16) int a; // 16字节对齐
    alignas(8) int b; // 8字节对齐
    alignas(4) int c; // 4字节对齐
    alignas(double) char buffer[1024]; // 按double类型的对齐要求对齐
    alignas(16) struct MyStruct { ... }; // 结构体对齐, 结构体总大小将是16的倍数
    alignas(32) int arr[10]; // 数组对齐

2. constexpr 在 C++14 中改进

C++14 对 constexpr 做了重大改进,显著放宽了限制,使其在实际编程中更加实用。以下是详细说明:

2.1 放宽函数体内的限制

  • C++11 中只能有一条 return 语句,不能有局部变量、循环、分支等

    1
    2
    3
    4
    constexpr int factorial(int n) 
    {
    return n <= 1 ? 1 : n * factorial(n - 1);
    }
  • C++14 放宽了限制,允许循环、允许使用局部变量并修改、允许使用 if-else等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    constexpr int factorial(int n) 
    {
    // C++14: 允许局部变量
    int result = 1;

    // C++14: 允许循环
    for (int i = 1; i <= n; ++i)
    {
    result *= i;
    }
    return result;
    }

    constexpr int abs_value(int x)
    {
    // C++14: 允许 if-else
    if (x < 0)
    {
    return -x;
    }
    else
    {
    return x;
    }
    }

2.2 constexpr 成员函数不再隐式为 const

在 C++11 中,被constexpr修饰的类成员函数会隐式成为 const 函数(常量成员函数),不能修改成员变量。

1
2
3
4
5
6
7
8
9
class Widget 
{
int value;
public:
// 显示定义的常量成员函数
constexpr int get() const { return value; }
// 错误!隐式常量成员函数, 不能在函数体重修改 value 的值
// constexpr void set(int v) { value = v; }
};

C++14中,constexpr修饰的类成员函数体内部可以修改类成员的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point 
{
int x, y;
public:
constexpr Point(int x, int y) : x(x), y(y) {}

// C++14: 可以是非 const 成员函数
constexpr void setX(int newX) { x = newX; } // 允许修改成员
constexpr void setY(int newY) { y = newY; }

constexpr int getX() const { return x; }
constexpr int getY() const { return y; }
};

// 使用示例
constexpr Point movePoint(Point p, int dx, int dy)
{
p.setX(p.getX() + dx);
p.setY(p.getY() + dy);
return p;
}

constexpr Point p1(10, 20);
constexpr Point p2 = movePoint(p1, 5, -5); // 编译时计算

2.3 允许 void 类型的 constexpr 函数

在C++14中,constexpr 函数可以返回 void,在 C++11 中该语法是不允许的。

1
2
3
4
5
6
7
8
9
10
11
12
13
constexpr void swap(int& a, int& b) 
{
int temp = a;
a = b;
b = temp;
}

constexpr int compute()
{
int x = 10, y = 20;
swap(x, y); // 调用返回 void 的 constexpr 函数
return x + y;
}

2.4 类的构造函数

在 C++11 中,constexpr 构造函数必须是空的,只能通过初始化列表初始化所有成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// C++11 的 constexpr 构造函数
class Point
{
private:
int x, y;

public:
// 必须:函数体为空,只有初始化列表
constexpr Point(int a, int b) : x(a), y(b)
{
// 函数体必须为空,不能有任何语句!
}

// 不允许有非 trivial 的构造函数体
// constexpr Point(int a, int b) : x(a), y(b) {
// if (a < 0) x = 0; // 错误:C++11 不允许
// }
};

C++14 允许 constexpr 构造函数有函数体,可以包含控制流语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// C++14 的 constexpr 构造函数
class Rectangle
{
private:
int width, height;

public:
// C++14 允许在函数体内有逻辑
constexpr Rectangle(int w, int h)
: width(w > 0 ? w : 1), // 可以在初始化列表中使用条件
height(h > 0 ? h : 1) {
// C++14 允许在函数体内添加逻辑
if (w > 1000 || h > 1000)
{
// 可以抛出异常(C++20 之前是条件性的)
width = 1000;
height = 1000;
}
// 甚至可以调用其他 constexpr 函数
validate();
}

private:
constexpr void validate()
{
// 可以在构造函数中调用
if (width * height > 1000000)
{
width = 1000;
height = 1000;
}
}
};

根据上文对讲解,我们可以对 constexprC++11C++14 中的特性做如下总结:

特性 C++11 C++14
构造函数体 必须为空 可以有任意语句
局部变量 ❌ 不允许 ✅ 允许
控制流语句 ❌ 不允许 ✅ 允许
函数调用 ❌ 不允许 ✅ 允许(调用 constexpr 函数)
循环 ❌ 不允许 ✅ 允许
修改类成员 只能在初始化列表 可以在函数体内

C++14 对 constexpr 的改进使得编译时编程成为 C++ 的强大特性,为模板元编程和编译时计算提供了更自然的语法。