只要有可能使用constexpr,就使用它
constexpr
表明的值不仅仅是常量,还是编译期可知的。这个表述并不全面,因为当constexpr
被用于函数的时候,事情就有一些差别了。
constexpr
对象和const
对象一样,它们是编译期可知的。
编译期可知的值,可能被存放到只读存储空间中。编译期可知的整数常量会出现在需要“整型常量表达式”的语境中,这类语境包括数组大小,整数模板参数(包括std::array
对象的长度),枚举量,对齐修饰符等等。如果想在这些语境中使用变量,一定要把它们声明为constexpr
,因为编译器会确保它们是编译期可知的。
int sz; //非constexpr变量
constexpr auto arraySize1 = sz; //错误!sz的值在
//编译器不可知
std::array<int, sz> data1; //错误
constexpr auto arraySize2 = 10; //没问题,10是编译器可知常量
std::array<int, arraySize2> data2; //没问题
注意,const
不提供constexpr
所能保证的事,因为const
对象不需要在编译器初始化它的值。
int sz;
const auto arraySize = sz;
std::array<int, arraySize> data; //错误,arraySize值在编译器不可知
简而言之,所有constexpr对象都是const,但不是所有const都是constexpr
对于函数来说,如果实参是编译期常量,它们将产出编译期值。如果是运行时值,它们就将产出运行时值。
- constexpr函数可以用于需求编译期常量的上下文。如果你传给constexpr函数的实参在编译期可知,那么结果将在编译期计算,如果实参的值在编译期不知道,你的代码就会被拒绝
- 当一个constexpr函数被一个或者多个编译期不可知值调用时,它就像普通函数一样,运行时计算它的结果,这意味着你不需要两个函数,一个用于编译期计算,一个用于运行时计算。constexpr全做了
constexpr int pow(int base, int exp) noexcept
{
...
}
constexpr auto numConds = 5;
std::array<int, pow(3, numConds)> results;
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中,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 Point p2(28.8, 5.3);
类似的,访问器xValue
和yValue
也可以声明为constexpr
,原因在于,若它们是通过一个在编译期已知其值的Point
对象,即一个constexpr Point
对象来调用的话,数据成员x和y的值就可以在编译期获知。这使得我们可以写一个constexpr
函数里面调用Point
的访问器并初始化constexpr
对象。
constexpr Point midpoint(const Point& p1, const Point& p2) noexcept
{
return { (p1.xValue() + p2.xValue()) / 2,
(p1.yValue() + p2.yValue()) / 2 };
}
constexpr auto mid = midpoint(p1, p2);
这意味着mid对象,尽管在其初始化过程中涉及了构造函数,访问器,还有非成员函数的调用,却仍可以在只读内存中得以创建。这意味着,你可以将一个类似mid.xValue()*10
的表达式运用到模板形参中,或是指定枚举量的表达式中。这也意味着以前相对严格的某一行代码只能用于编译期,某一行代码只能用于运行时的界限变得模糊,一些运行时的普通计算能并入编译时。越多这样的代码并入,你的程序就越快。
在C++11中,有两个限制使得Point的成员函数setX
和setY
不能声明为constexpr
。第一,它们修改了它们操作的对象的状态,并且在C++11中,constexpr
成员函数是隐式的const
,第二,它们只能有void
返回类型,void
类型不是C++11中的字面值类型。这两个限制在C++14中放开了,所以C++14中Point
的set
函数也能声明为constexpr
。
class Point{
public:
...
constexpr void setX(double newX) noexcept { x = newX; }
constexpr void setY(double newY) noexcept { y = newY; }
...
};
现在也可以写这样的函数
constexpr Point reflection(const Point& p) noexcept
{
Point result;
result.setX(-p.xValue());
result.setY(-p.yValue());
return result;
}
客户端代码可以这样写
constexpr Point p1(1,1);
constexpr Point p2(2,2);
constexpr auto mid = midpoint(p1, p2);
constexpr auto reflectedMid = reflection(mid);
要点速记
constexpr
对象是const
,它的值在编译期可知- 当传递编译期可知的值时,
constexpr
函数可以产生编译期结果