item15¶
条款十五:尽可能的使用 constexpr
¶
Item 15: Use constexpr
whenever possible
如果要给 C++11 颁一个“最令人困惑新词”奖,constexpr
十有八九会折桂。当用于对象上面,它本质上就是 const
的加强形式,但是当它用于函数上,意思就大不相同了。有必要消除困惑,因为你绝对会用它的,特别是当你发现 constexpr
“正合吾意”的时候。
从概念上来说,constexpr
表明一个值不仅仅是常量,还是编译期可知的。这个表述并不全面,因为当 constexpr
被用于函数的时候,事情就有一些细微差别了。为了避免我毁了结局带来的 surprise,我现在只想说,你不能假设 constexpr
函数的结果是 const
,也不能保证它们的(译注:返回)值是在编译期可知的。最有意思的是,这些是 特性。关于 constexpr
函数返回的结果不需要是 const
,也不需要编译期可知这一点是 良好的 行为!
不过我们还是先从 constexpr
对象开始说起。这些对象,实际上,和 const
一样,它们是编译期可知的。(技术上来讲,它们的值在翻译期(translation)决议,所谓翻译不仅仅包含是编译(compilation)也包含链接(linking),除非你准备写 C++ 的编译器和链接器,否则这些对你不会造成影响,所以你编程时无需担心,把这些 constexpr
对象值看做编译期决议也无妨的。)
编译期可知的值“享有特权”,它们可能被存放到只读存储空间中。对于那些嵌入式系统的开发者,这个特性是相当重要的。更广泛的应用是“其值编译期可知”的常量整数会出现在需要“整型常量表达式(integral constant expression)的上下文中,这类上下文包括数组大小,整数模板参数(包括 std::array
对象的长度),枚举名的值,对齐修饰符(译注:alignas(val)
),等等。如果你想在这些上下文中使用变量,你一定会希望将它们声明为 constexpr
,因为编译器会确保它们是编译期可知的:
int sz; //non-constexpr变量
…
constexpr auto arraySize1 = sz; //错误!sz的值在
//编译期不可知
std::array<int, sz> data1; //错误!一样的问题
constexpr auto arraySize2 = 10; //没问题,10是
//编译期可知常量
std::array<int, arraySize2> data2; //没问题, arraySize2是constexpr
注意 const
不提供 constexpr
所能保证之事,因为 const
对象不需要在编译期初始化它的值。
cpp
int sz; //和之前一样
…
const auto arraySize = sz; //没问题,arraySize是sz的const复制
std::array<int, arraySize> data; //错误,arraySize值在编译期不可知
简而言之,所有 constexpr
对象都是 const
,但不是所有 const
对象都是 constexpr
。如果你想编译器保证一个变量有一个值,这个值可以放到那些需要编译期常量(compile-time constants)的上下文的地方,你需要的工具是 constexpr
而不是 const
。
涉及到 constexpr
函数时,constexpr
对象的使用情况就更有趣了。如果实参是编译期常量,这些函数将产出编译期常量;如果实参是运行时才能知道的值,它们就将产出运行时值。这听起来就像你不知道它们要做什么一样,那么想是错误的,请这么看:
+ constexpr
函数可以用于需求编译期常量的上下文。如果你传给 constexpr
函数的实参在编译期可知,那么结果将在编译期计算。如果实参的值在编译期不知道,你的代码就会被拒绝。
+ 当一个 constexpr
函数被一个或者多个编译期不可知值调用时,它就像普通函数一样,运行时计算它的结果。这意味着你不需要两个函数,一个用于编译期计算,一个用于运行时计算。constexpr
全做了。
假设我们需要一个数据结构来存储一个实验的结果,而这个实验可能以各种方式进行。实验期间风扇转速,温度等等都可能导致亮度值改变,亮度值可以是高,低,或者无。如果有 n 个实验相关的环境条件,它们每一个都有三个状态,最终可以得到的组合有 3n个。储存所有实验结果的所有组合需要足够存放 3n个值的数据结构。假设每个结果都是 int
并且 n 是编译期已知的(或者可以被计算出的),一个 std::array
是一个合理的选择。我们需要一个方法在编译期计算 3n。C++ 标准库提供了 std::pow
,它的数学功能正是我们所需要的,但是,对我们来说,这里还有两个问题。第一,std::pow
是为浮点类型设计的,我们需要整型结果。第二,std::pow
不是 constexpr
(即,不保证使用编译期可知值调用而得到编译期可知的结果),所以我们不能用它作为 std::array
的大小。
幸运的是,我们可以应需写个 pow
。我将展示怎么快速完成它,不过现在让我们先看看它应该怎么被声明和使用:
constexpr //pow是绝不抛异常的
int pow(int base, int exp) noexcept //constexpr函数
{
… //实现在下面
}
constexpr auto numConds = 5; //(上面例子中)条件的个数
std::array<int, pow(3, numConds)> results; //结果有3^numConds个元素
回忆下 pow
前面的 constexpr
不表明 pow
返回一个 const
值,它只说了如果 base
和 exp
是编译期常量,pow
的值可以被当成编译期常量使用。如果 base
和/或 exp
不是编译期常量,pow
结果将会在运行时计算。这意味着 pow
不止可以用于像 std::array
的大小这种需要编译期常量的地方,它也可以用于运行时环境:
auto base = readFromDB("base"); //运行时获取这些值
auto exp = readFromDB("exponent");
auto baseToExp = pow(base, exp); //运行时调用pow函数
因为 constexpr
函数必须能在编译期值调用的时候返回编译期结果,就必须对它的实现施加一些限制。这些限制在 C++11 和 C++14 标准间有所出入。
C++11 中,constexpr
函数的代码不超过一行语句:一个 return
。听起来很受限,但实际上有两个技巧可以扩展 constexpr
函数的表达能力。第一,使用三元运算符“?:
”来代替 if
-else
语句,第二,使用递归代替循环。因此 pow
可以像这样实现:
constexpr int pow(int base, int exp) noexcept
{
return (exp == 0 ? 1 : base * pow(base, exp - 1));
}
这样没问题,但是很难想象除了使用函数式语言的程序员外会觉得这样硬核的编程方式更好。在 C++14 中,constexpr
函数的限制变得非常宽松了,所以下面的函数实现成为了可能:
constexpr int pow(int base, int exp) noexcept //C++14
{
auto result = 1;
for (int i = 0; i < exp; ++i) result *= base;
return result;
}
constexpr
函数限制为只能获取和返回 字面值类型,这基本上意味着那些有了值的类型能在编译期决定。在 C++11 中,除了 void
外的所有内置类型,以及一些用户定义类型都可以是字面值类型,因为构造函数和其他成员函数可能是 constexpr
:
class Point {
public:
constexpr Point(double xVal = 0, double yVal = 0) noexcept
: x(xVal), y(yVal)
{}
constexpr double xValue() const noexcept { return x; }
constexpr double yValue() const noexcept { return y; }
void setX(double newX) noexcept { x = newX; }
void setY(double newY) noexcept { y = newY; }
private:
double x, y;
};
Point
的构造函数可被声明为 constexpr
,因为如果传入的参数在编译期可知,Point
的数据成员也能在编译器可知。因此这样初始化的 Point
就能为 constexpr
:
constexpr Point p1(9.4, 27.7); //没问题,constexpr构造函数
//会在编译期“运行”
constexpr Point p2(28.8, 5.3); //也没问题
类似的,xValue
和 yValue
的 getter(取值器)函数也能是 constexpr
,因为如果对一个编译期已知的 Point
对象(如一个 constexpr
Point
对象)调用 getter,数据成员 x
和 y
的值也能在编译期知道。这使得我们可以写一个 constexpr
函数,里面调用 Point
的 getter 并初始化 constexpr
的对象:
constexpr
Point midpoint(const Point& p1, const Point& p2) noexcept
{
return { (p1.xValue() + p2.xValue()) / 2, //调用constexpr
(p1.yValue() + p2.yValue()) / 2 }; //成员函数
}
constexpr auto mid = midpoint(p1, p2); //使用constexpr函数的结果
//初始化constexpr对象
这太令人激动了。它意味着 mid
对象通过调用构造函数,getter 和非成员函数来进行初始化过程就能在只读内存中被创建出来!它也意味着你可以在模板实参或者需要枚举名的值的表达式里面使用像 mid.xValue() * 10
的表达式!(因为 Point::xValue
返回 double
,mid.xValue() * 10
也是个 double
。浮点数类型不可被用于实例化模板或者说明枚举名的值,但是它们可以被用来作为产生整数值的大表达式的一部分。比如,static_cast<int>(mid.xValue() * 10)
可以被用来实例化模板或者说明枚举名的值。)它也意味着以前相对严格的编译期完成的工作和运行时完成的工作的界限变得模糊,一些传统上在运行时的计算过程能并入编译时。越多这样的代码并入,你的程序就越快。(然而,编译会花费更长时间)
在 C++11 中,有两个限制使得 Point
的成员函数 setX
和 setY
不能声明为 constexpr
。第一,它们修改它们操作的对象的状态, 并且在 C++11 中,constexpr
成员函数是隐式的 const
。第二,它们有 void
返回类型,void
类型不是 C++11 中的字面值类型。这两个限制在 C++14 中放开了,所以 C++14 中 Point
的 setter(赋值器)也能声明为 constexpr
:
class Point {
public:
…
constexpr void setX(double newX) noexcept { x = newX; } //C++14
constexpr void setY(double newY) noexcept { y = newY; } //C++14
…
};
现在也能写这样的函数:
//返回p相对于原点的镜像
constexpr Point reflection(const Point& p) noexcept
{
Point result; //创建non-const Point
result.setX(-p.xValue()); //设定它的x和y值
result.setY(-p.yValue());
return result; //返回它的副本
}
客户端代码可以这样写:
constexpr Point p1(9.4, 27.7); //和之前一样
constexpr Point p2(28.8, 5.3);
constexpr auto mid = midpoint(p1, p2);
constexpr auto reflectedMid = //reflectedMid的值
reflection(mid); //(-19.1, -16.5)在编译期可知
本条款的建议是尽可能的使用 constexpr
,现在我希望大家已经明白缘由:constexpr
对象和 constexpr
函数可以使用的范围比 non-constexpr
对象和函数大得多。使用 constexpr
关键字可以最大化你的对象和函数可以使用的场景。
还有个重要的需要注意的是 constexpr
是对象和函数接口的一部分。加上 constexpr
相当于宣称“我能被用在 C++ 要求常量表达式的地方”。如果你声明一个对象或者函数是 constexpr
,客户端程序员就可能会在那些场景中使用它。如果你后面认为使用 constexpr
是一个错误并想移除它,你可能造成大量客户端代码不能编译。(为了 debug 或者性能优化而添加 I/O 到一个函数中这样简单的动作可能就导致这样的问题,因为 I/O 语句一般不被允许出现在 constexpr
函数里)“尽可能”的使用 constexpr
表示你需要长期坚持对某个对象或者函数施加这种限制。
请记住:
constexpr
对象是const
,它被在编译期可知的值初始化- 当传递编译期可知的值时,
constexpr
函数可以产出编译期可知的结果 constexpr
对象和函数可以使用的范围比 non-constexpr
对象和函数要大constexpr
是对象和函数接口的一部分