文章目录
下面的 MaxValue
函数,它的功能是返回两个整形变量里较大的那个。
const int& MaxValue(const int& a, const int& b) {
return a>b?a:b;
}
把这种规模较小的操作定义成函数有很多好处,主要包括:
- 阅读和理解
MaxValue
函数的调用要比读懂等价的条件表达式容易得多。 - 使用函数可以确保行为的统一,每次相关操作都能保证按照同样的方式进行。
- 如果我们需要修改计算过程,显然修改函数要比先找到等价表达式所有出现的地方再逐一修改更容易
- 函数可以被其他应用重复利用,省去了程序员重新编写的代价
然而,使用 MaxValue
函数也存在一个潜在的缺点:调用函数一般比求等价表达式的值要慢一些。
在大多数机器上,一次函数调用其实包含着一系列工作:
- 调用前要先保存寄存器,并在返回时恢复;
- 可能需要拷贝实参;
- 程序转向一个新的位置继续执行。
内联函数可避免函数调用的开销
将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。假设我们把 MaxValue
函数定义为内联函数,则如下调用
cout << MaxValue(x1, x2) << endl;
将在编译过程中展开成类似于下面的形式
cout << x1 > x2 ? x1 : x2 << endl;
从而消除了 MaxValue
函数的运行时开销。
在 MaxValue
函数的返回类型前面加上关键字 inline
,这样就可以将它声明成内联函数了:
// 内联版本:寻找两个整形值中较大的值
inline const int& MaxValue(const int& a, const int& b)
{
return a>b?a:b;
}
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个 75 行的函数也不大可能在调用点内联地展开。
constexpr
constexpr 和常量表达式
常量表达式(const expression) 是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的 const
对象也是常量表达式。后面会提到,C++ 语言中有几种情况下是要用到常量表达式的。
一个对象(或表达式)是不是常量表达式由它们的数据类型和初始值共同决定,例如:
const int max_files = 20; // max_files 是常量表达式
const int limit = max_files + 1; // limit 是常量表达式
int staff_size = 27; // staff_size 不是常量表达式
const int sz = get_size(); // sz 不是常量表达式
尽管 staff_size
的初始值是个字面值常量,但由于它的数据类型只是一个普通 int 而非 const int,所以它不属于常量表达式。另一方面,尽管 sz
本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。
constexpr 变量
在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。当然可以定义一个 const 变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。可以这么说,在此种情况下,对象的定义和使用根本就是两回事儿。const得在运行时才能确定其值,constexpr在编译时就确定下来了。
比如下面的例子,常量 sz
在运行时才能确定下来
#include <iostream>
int get_size(int i)
{
return i;
}
int main()
{
int staff_size;
std::cin >> staff_size;
const int sz = get_size(staff_size); // sz 不是常量表达式
std::cout << sz << std::endl;
}
C++11 新标准规定,允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。声明为 constexpr
的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20; // 20 是常量表达式
constexpr int limit = mf + 1; // mf + 1是常量表达式
constexpr int sz = size(); // 只有当 size 是一个 constexpr 函数时才是一个正确的声明语句
尽管不能使用普通函数作为 constexpr
变量的初始值,新标准允许定义一种特殊的 constexpr
函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用 constexpr
函数取初始化 constexpr
变量了。
一般来说,如果你认定变量是一个常量表达式,那就把它声明成 constexpr 类型。
字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明 constexpr
时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为 ”字面值类型“(literal type)。
到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。自定义类 Sales_item
、IO 库、string 类型则不属于字面值类型,也就不能被定义为 constexpr
。
尽管指针和引用都能定义成 constexpr
,但它们的初始值却受到严格限制。一个 constexpr 指针的初始值必须是 nullptr 或者 0,或者是存储于某个固定地址中的对象。
函数体内定义的变量一般来说并非存放在固定地址中,因此 constexpr 指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用初始化 constexpr 指针。允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr 引用能绑定到这样的变量上,constexpr 指针也能指向这样的变量。
指针和constexpr
必须明确一点,在 constexpr 声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关:
const int *p = nullptr; // p 是一个指向整型常量的指针
constexpr int *q = nullptr; // q 是一个指向整数的常量指针
p
和 q
的类型相差甚远,p
是一个指向常量的指针,而 q
是一个常量指针,其中的关键在于 constexpr 把它所定义的对象置为了顶层 const。
与其他常量指针类似,constexpr 指针既可以指向常量也可以指向一个非常量:
constexpr int *np = nullptr; // np 是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 42; // i 的类型是整形常量
// i 和 j 都必须定义在函数体之外
constexpr const int *p = &i; // p 是常量指针,指向整形常量 i
constexpr int *p1 = &j; // p1 是常量指针,指向整数 j
constexpr 函数
constexpr函数(constexpr function) 是指能用于常量表达式的函数。定义 constexpr 函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return 语句:
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz(); // 正确:foo 是一个常量表达式
我们把 new_sz
定义成无参数的 constexpr 函数。因为编译器能在程序编译时验证 new_sz
函数返回的是常量表达式,所以可以用 new_sz
函数初始化 constexpr 类型的变量 foo
。
执行该初始化任务时,编译器把对 constexpr 函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr 函数被隐式地指定为内联函数。
constexpr 函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr 函数中可以有空语句、类型别名以及 using 声明。
我们允许 constexpr 函数的返回值并非一个常量:
// 如果 arg 是常量表达式,则 scale(arg) 也是常量表达式
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
当 scale
的实参是常量表达式时,它的返回值也是常量表达式;反之则不然:
int arr[scale(2)]; // 正确:scale(2) 是常量表达式
int i=2; // i不是常量表达式
int a2[scale(i)]; // 错误:scale(i) 不是常量表达式
如上例所示,当我们给 scale
函数传入一个形如字面值 2 的常量表达式时,它的返回类型也是常量表达式。此时,编译器用相应的结果值替换对 scale
函数的调用。
如果我们用一个非常量表达式调用 scale
函数,比如 int 类型的对象 i
,则返回值是一个非常量表达式。当把 scale
函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要求。如果结果恰好不是常量表达式,编译器将发出错误信息。
constexpr 函数不一定返回常量表达式
把内联函数和 constexpr 函数放在头文件内
和其他函数不一样,内联函数和 constexpr 函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者 constexpr 函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和 constexpr 函数通常定义在头文件中。