从编译期常量谈起
C++20都支持虚函数的constexpr了,我打算用三篇读文章讲清楚编译期常量和constexpr这个东西和编译期常量的关系,即为什么需要他来辅助解决这个问题。最后帮助读者在实际编码过程中能够有意识地去运用他们,这才是终极目标。这篇文章中会讲到隐藏在日常编程中的各种编译期常量,以及他们存在的意义。
什么是编译期常量
想要用编译期常量就要首先知道它们是什么,一般出现在哪里和运行期常量有什么区别,因此我打算用第一篇文章重点分析编译期常量以及使用他们有什么好处。
编译期常量(Compile-time constants)是C++中相当重要的一部分,整体而言他们有助提高程序的正确性,并提高程序的性能。这篇文章中出现的编译期常量都是在C++11之前就可以使用的,constexpr是C++11的新特性,所以各位不要有心理包袱。
总有些东西是编译器要求编译期间就要确定的,除了变量的类型外,最频繁出现的地方就是数组、switch的case标签和模板了。
数组中的编译期常量
如果我们想要创建一个不是动态分配内存的数组,那么我们就必须给他设定一个size——这个size必须在编译期间就知道,因此静态数组的大小是编译期常量。
1
int someArray[520];
只有这么做,编译器才能准确地解算出到底要分配给这个数组多少内存。如果这个数组在函数中,数组的内存就会被预留在该函数的栈帧中;如果这个数组是类的一个成员,那么编译器要确定数组的大小以确定这个类成员的大小——无论哪种情况,编译器都要知道这个数组具体的size。
有些时候我们不用显示得指明数组的大小,我们用字符串或花括号来初始化数组的时候,编译器会实现帮我们数好这个数组的大小。
1
2
int someArray[] = {5, 2, 0};
char charArray[] = "Ich liebe dich.";
模板中的编译期常量
除了类型以外,数字也可以作为模板的参数。这些数值变量包括int,long,short,bool,char和弱枚举enum等。
1
2
3
4
5
6
enum Color {RED, GREEN, BLUE};
template<unsigned long N, char ID, Color C>
struct someStruct {};
someStruct<42ul, 'e', GREEN> theStruct;
Case labels
既然编译器在初始化模板的时候必须知道模板的类型,那么这些模板的参数也必须是编译期常量。
switch语句的分支判断也必须是编译期常量,和上边模板的情况非常类似。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void comment(int phrase) {
switch(phrase) {
case 42:
std::cout << "You are right!" << std::endl;
break;
case BLUE:
std::cout << "Don't be upset!" << std::endl;
break;
case 'z':
std::cout << "You are the last one!" << std::endl;
break;
default:
std::cout << "This is beyond what I can handle..." << std::endl;
}
}
使用编译期常量有什么好处
如果编译期常量的使用方法只有上边呈现的几种,那你大概会感觉有些无聊了。事实上,关于编译期常量我们能做的事情还有许多,他们能帮助我们去实现更高效的程序。
更安全的程序
编译期常量能让我们写出更有逻辑的代码——在编译期就体现出逻辑。比如矩阵相乘:
1
2
3
4
5
class Matrix{
unsigned rowCount;
unsigned columnCount;
//...
};
我们都知道,两个矩阵相乘,当且仅当左矩阵的列数等于右矩阵的行数,如果不满足这个规则的话,那就完蛋了,所以针对上边矩阵的乘法,我们在函数中要做一些判断:
1
2
3
4
5
6
7
Matrix operator*(Matrix const& lhs, Matrix const& rhs) {
if(lhs.getColumnCount() != rhs.getRowCount()) {
throw OhWeHaveAProblem();
}
//...
}
但是如果我们在编译期就知道了矩阵的size,那么我们就可以把上边的判断放在模板中完成——这样的话不同size的矩阵一下子就成了不同类型的变量了。这样我们的矩阵乘法也相应变得简单了一些:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <unsigned Rows, unsigned Columns>
class Matrix {
/* ... */
};
template <unsigned N, unsigned M, unsigned P>
Matrix<N, P> operator*(Matrix<N, M> const& lhs, Matrix<M, P> const& rhs) {
/* ... */
}
Matrix<1, 2> m12 = /* ... */;
Matrix<2, 3> m23 = /* ... */;
auto m13 = m12 * m23; // OK
auto mX = m23 * m13; // Compile Error!
在这个例子中,编译器本身就阻止了错误的发生,还有很多其他的例子——更复杂的例子在编译期间使用模板。从C++11后有一堆这样的模板都定义在了标准库STL中,这个之后再说。所以大家不要觉得上边这种做法是脱裤子放屁,相当于我们把运行时的条件判断交给了编译期来做,前提就是矩阵的类型必须是编译期常量。你可能会问,除了像上边直接用常数来实例化矩阵,有没有其他方法来告诉编译器这是个编译期常量呢?请往下看。
编译优化
编译器能根据编译期常量来实现各种不同的优化。比如,如果在一个if判断语句中,其中一个条件是编译期常量,编译器知道在这个判断句中一定会走某一条路,那么编译器就会把这个if语句优化掉,留下只会走的那一条路。
1
2
3
4
5
if (sizeof(void*) == 4) {
std::cout << "This is a 32-bit system!" << std::endl;
} else {
std::cout << "This is a 64-bit system!" << std::endl;
}
在上例中,编译器就会直接利用其中某一个cout语句来替换掉整个if代码块——反正运行代码的机器是32还是64位的又不会变。 另一个可以优化的地方在空间优化。总体来说,如果我们的对象利用编译期常数来存储数值,那么我们就不用在这个对象中再占用内存存储这些数。就拿本文之前的例子来举例:
- someStruct结构中包含一个‘unsigned long’,一个‘char’,和一个‘color’,尽管如此他的实例对象却只占用一个byte左右的空间。
- 矩阵相乘的时候,我们在矩阵中也没必要花费空间去存储矩阵的行数和列数了。
结语
这一篇文章只讲到了编译期常量,为了使编译器在编译期间计算出常量,我们在C++11标准之前和之后都采用了不同的方法去实现它。在第二篇文章中,我会将主要精力放在C++11标准之前的编译期计算的问题,通过展现一系列蹩脚的方法来引出我们的主角——constexpr。