C++干货系列——从编译期常量谈到constexpr(二)

本文介绍了在C++11标准之前,如何利用编译期常量进行编译期运算,包括字面量、枚举量和sizeof操作符等来源。通过示例展示了如何通过静态成员变量和模板实现编译期计算,并探讨了模板元编程的基本思想,以Fibonacci数列为例展示了模板的编译期计算能力。尽管这种方法在C++11之后有了更好的替代(如constexpr),但在C++11之前,它是实现编译期复杂计算的主要途径。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++11标准前的编译期运算

在第一篇文章中,我把主要精力放在了什么是编译期常量,以及编译期常量有什么作用上。在这一篇文章中,我将更详细地介绍编译期常量是如何产生的。之所以要把编译期常量了解的这么透彻,是因为他是编译期运算的基础。在这篇文章中还会讲解我们在C++11 标准前都可以用做哪些**编译期运算(**compile-time calculations),通过了解这些比较原始的方法,我们能够更好地理解C++11标准为编译期运算方面所做的工作。

编译期常量都从哪里来?

在我们的经验中,大部分编译期常量的来源还是字面常量(literals)以及枚举量(enumerations)。比如上一篇文章我写的someStruct<42ul, 'e', GREEN> theStruct;someStruct的三个模板参数都是常量——分别是整形字面量、char型字面量和枚举常量。

比较典型的编译期常量的来源就是内置的sizeof操作符。编译器必须在编译期就知道一个变量占据了多少内存,所以它的值也可以被用作编译期常量。

 class SomeClass {
   //...
 };
 int const count = 10;  //作为数组的size,编译期常量
 SomeClass theMovie[count] = { /* ... */}; //常量表达式,在编译期计算
 int const otherConst = 26; //只是常量,但不是编译期常量
 
 int i = 419;
 unsigned char buffer[sizeof(i)] = {};   //常量表达式,在编译期计算

另一个经常出现编译期常量最常出现的地方就是静态类成员变量(static class member variables),而枚举常量常常作为它的替换也出现在类中。

 struct SomeStruct{
   static unsigned const size1 = 44;  //编译期常量
   enum { size2 = 45 };  //编译期常量
   int someIntegers[size1];  //常量表达式,在编译期计算
   double someDoubles[size2]; //常量表达式,在编译期计算
 };

与编译期常量对应的概念编译期常量表达式(compile-time constant expression)指的是,值不会改变且在编译期就可以计算出来的表达式。其实更好理解的说法是,任何不是用户自己定义的——而必须通过编译期计算出来的字面量都属于编译期常量表达式。需要注意的是,并不是所有的常量表达式都是编译期常量表达式,只有我们要求编译器计算出来时,才是编译期常量表达式。希望下边这个例子可以做很好的说明:我们通过把p安排在合适的位置——数组的size,强制编译器去计算p的值,即p此时变成了编译期常量表达式。

const int i = 100;        
const int j = i * 200;    //常量表达式,但不是编译期常量表达式

const int k = 100;        
const int p = k * 200;    //是编译期常量表达式,由下边数组确定
unsigned char helper[p] = {}; //要求p是编译期常量表达式,在编译期就需确定

编译期运算

从上边的例子可以看出,有时我们可以通过某些手段去“胁迫”编译器,把运算任务从运行时提前到编译期,这就是编译期运算的原理。正如“常量表达式”这个名字,我们可以做各种各样的编译期运算,实现在编译期就确定一个常量表达式的目的。事实上,由最简单的运算表达式出发,我们可以做到各种各样的编译期运算。比如非常简单:

 int const doubleCount = 10;
 unsigned char doubleBuffer[doubleCount * sizeof(double)] = {};

除此之外,我们也可以用许多其他的操作,比如考虑下边并没有什么意义的代码:

 std::string nonsense(char input) {
   switch(input) {
   case "some"[(sizeof(void*) == 4) ? 0 : 1]:
     return "Aachen";
   default:
     return "Wuhan";
   }
 }

上边的代码并没有什么实际的意义,但是我还是想解释一下。在上一篇文章我们解释过了,switch语句的每一个case label必须是编译期常量,表达式sizeof(void*) == 4的意思是当前系统是不是一个32位系统,这个表达式由于sizeof的原因是常量表达式,判断结果作为三元运算符的第一个参数,最后的case label由当前系统的位数分别是"some"的"s"(是32位系统)或"o"(不是32位系统)。返回的两个字符串分别是我的两个学校的城市。

尽管上边的例子是无意义的,我们仍然可以看出由这种方法写出的常量表达式很难读。我们可以改进可读性,将上边例子改写成:

 std::string nonsense(char input) {
   auto const index = (sizeof(void*) == 4) ? 0 : 1;
   auto const someLabel = "some"[index];
   switch(input) {
   case someLabel:
     return "Aachen";
   default:
     return "Wuhan";
   }
 }

使用模板进行编译期运算

在上篇文章我们提到,实例化模板的参数必须为编译期常数——换句话说编译器会在编译期计算作为实例化模板参数的常量表达式。回忆一下我们可以利用静态成员常量作为编译期常量,我们就可以利用以上特性去把函数模板当成函数来计算,其实这就是模板元编程(template meta programming)方法的雏形。

 template <unsigned N> 
 struct Fibonacci;
 
 template <>
 struct Fibonacci<0> {
   static unsigned const value = 0;   
 };
 
 template <>
 struct Fibonacci<1> {
   static unsigned const value = 1;   
 };
 
 template <unsigned N> 
 struct Fibonacci {
   static unsigned const value = Fibonacci<N-1>::value + Fibonacci<N-2>::value;
 };

最后一个模板比较有意思,仔细看代码就会发现,它递归式地去实例化参数为N的的模板,递归终止在模板参数为01时,就是我们的第二和第三个模板所直接返回的编译期常量。

这种模板元函数看起来啰啰嗦嗦的,但是在C++11出现前,它是唯一可以在编译期进行复杂编译期运算的方法。虽然它已经被证实为图灵完备的,但是往往编译器在递归的时候还是会设置一个模板最大初始化深度来避免无穷编译期递归。

结论

正如上所示,即使在C++11前,编译器运算已经可以大有作为了。别忘了所有我们所做的编译期运算的工作都是在为运行期减少负担。

在C++11和C++14中,一方面,可变参数模板的出现让更为复杂的模板元编程成为了可能;另一方面,constexpr的出现也完全改变了我们使用编译期常量的思路。在下一篇文章中,我们会着重介绍constexpr这个实战利器。

<think>好的,我现在需要回答用户关于声明编译期常量表达式的编程关键字的问题。首先,我要确认用户的问题核心是什么。用户想知道在不同编程语言中,特别是C++,用于声明编译期常量表达式的关键字有哪些。 根据提供的引用内容,引用了多个关于C++常量表达式的资料。比如引用[1]提到了C++11中的constexpr和const的区别,引用[2]详细解释了常量表达式的定义,引用[3]讨论了constexpr函数和对象,而引用[4]则涉及编译期字符串处理。这说明用户可能主要关注C++,但可能对其他语言也有兴趣。 首先,我应该先整理C++中的相关关键字。在C++中,const和constexpr都是用于声明常量的关键字,但它们的用途不同。constexprC++11引入的,用于指示编译器该表达式在编译时求值,而const只是表示变量不可修改,但可能在运行时初始化。需要明确区分这两者的区别,并举例说明。 接下来,可能需要提到其他编程语言中的类似关键字。比如Java中的final,C#中的const和readonly,JavaScript的const,TypeScript的const和readonly,以及Rust的const和static。这些关键字在不同语言中的行为不同,需要简要说明每个的作用。 然后,要确保回答结构清晰,分点列出,每个语言单独说明,特别是C++部分需要详细展开,因为用户提供的引用主要围绕C++。同时,需要引用提供的资料来支持回答,比如引用[2]中提到的常量表达式定义,引用[3]中的constexpr函数和对象的作用。 需要注意的是,用户的问题可能不仅限于C++,但根据提供的引用,重点应在C++上。因此,在回答中需要优先详细说明C++的情况,再简要概述其他语言,以满足可能的扩展需求。 另外,用户可能对关键字的具体用法和示例感兴趣,所以在每个部分加入代码示例会更好。例如,C++中的constexpr变量和函数的使用示例,Java中的final变量等。 最后,生成的相关问题需要围绕编译期常量表达式的关键字展开,可能涉及不同语言的对比、使用场景、优势等。确保问题能够引导用户进一步探索该主题。</think>在编程语言中,声明**编译期常量表达式**的关键字主要用于标识那些在编译阶段即可确定值的常量。以下是不同语言中常见的相关关键字及用法: --- ### **C++** 1. **`constexpr`**(C++11起) 用于声明变量、函数或对象构造函数为**编译期常量表达式**,要求其值在编译时即可计算。 ```cpp constexpr int a = 10; // 编译期常量 constexpr int square(int x) { // 编译期常量表达式函数 return x * x; } constexpr int b = square(5); // 编译期计算结果为25 ``` 若函数参数为常量表达式,则函数会在编译期执行[^3]。 2. **`const`** 表示变量不可修改,但初始化可能发生在运行期(如依赖运行时输入)。仅当初始化为编译期常量时,`const`才等效于`constexpr`[^1][^2]: ```cpp const int c = 10; // 编译期常量(等价于constexpr) const int d = get_val(); // 运行期初始化,非编译期常量 ``` --- ### **其他语言** 1. **Java** - **`final`**:声明不可变变量,若初始值为编译期常量(如字面量),则等效于编译期常量。 ```java final int a = 10; // 编译期常量 final int b = a * 2; // 编译期计算为20 ``` 2. **C#** - **`const`**:严格限定编译期常量,仅支持基本类型和字符串。 - **`readonly`**:运行期常量,初始化在构造函数中完成。 ```csharp const int a = 10; readonly int b = 20; // 运行期初始化 ``` 3. **JavaScript/TypeScript** - **`const`**:声明不可重新赋值的变量,但值本身可能是运行期计算的(如对象引用)。 - **TypeScript的`readonly`**:用于接口或类属性,限制修改。 4. **Rust** - **`const`**:编译期常量,值必须在编译时确定。 - **`static`**:静态变量,可包含运行期初始化的值。 --- ### **核心区别** - **编译期 vs 运行期**:`constexpr`(C++)、`const`(C#)等强制编译期求值;`const`(C++)、`readonly`(C#)等可能涉及运行期。 - **应用场景**:编译期常量可用于数组长度、模板参数等需要编译期确定值的场景[^2][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值