如果有人问在C++11引入的众多关键字中,有哪个关键字是最让你迷惑的,我会立马回答是constexpr
,当这个关键字应用到对象上的时候,它其实就是一个加强版的const
,但是当它应用到函数身上,它就有了不同的含义。为此很有必要去深入学习constexpr
解除其迷惑,使得我们在使用它的时候是符合预期的。
从概念上讲,constexpr
修饰的值不仅仅是一个常量值,而且还是一个编译期(参见runtime-vs-compile-time)就知道的值,相比于const
来说有几点区别,其一就是const
修饰的变量可以是编译期的值,也可以是运行期的值。而constexpr
必须是编译期的值,其二就是const
只能用于修饰类的成员函数,而constexpr
可以修饰普通的函数,并且两者的含义完全不同。首先让我们先来认识一下constexpr
的最基本的功能。
int sz;
constexpr auto arraySize1 = sz; // error sz's value not known at compilation
std::array<int, sz> data1 // error array size need compilation value
constexpr auto arraySize2 = 10; // fine
std::array<int, arraySize2> data2; // fine
constexpr
修饰的变量一定要求是编译期的值,而const
没有这个要求。通过这个事实我们可以知道是constexpr
的变量一定是const
,反之则不然。那么一切使用const
的地方其实都可以替换为constexpr
这也是本文的题目所要表达的含义。
我们都知道在C++
中有些地方必须要传入编译期的值,比如数组的大小,˙整型模版参数(标准库的bitset容器),枚举值,对齐大小等等。在这些地方如果你想传入一个编译期的值,const是无法保证的,除非你确定你const修饰的变量都是编译期的值,倘若后面修改代码使得变成非编译期的值,将会导致大量编译错误,而这些地方使用constexpr
是最万无一失的了,因为编译器会确保constexpr
是编译期值。
最后我们来谈一下constexpr
的一个与众不同的地方,就是修饰函数,和const
完全不同,但是却隐式的包含了const
的功能,这再次验证了,凡是出现const
的地方其实都是可以替换为constexpr
。constexpr
修饰的函数可以使得该函数可以在编译期运行,产生一个编译期的值,也可以像普通的函数一样在运行期执行,然后产生一个运行期的值。这很大程度上取决于传入的参数是编译期的值还是运行期的值。下面是一个例子:
constexpr int pow(int base, int exp) noexcept
{
return 10;
}
int base = 10;
constexpr constexpr_base data3 = 10;
std::array<int,pow(1,2)> data1; // ok
std::array<int,pow(base,2) data2; //compile error
std::array<int,pow(constexpr_base,2) data3; //ok
pow
的实现在这里先暂时直接返回,然后当传入pow
的参数是编译期值的时候是可以作为数组的长度的,否则会编译失败。接下来我们来实现一下pow
。
constexpr int pow (int base, int exp) noexcept
{
auto result = 1;
for(int i = 0;i < exp; ++i) result *= base;
return result;
}
很不幸,上面这个版本的pow
并不work
,会出现很多编译的错误如下:
pow.cc:7:3: error: statement not allowed in constexpr function
for (int i = 0; i < exp; ++i) result *= base;
意思就是for
这个语句无法在constexpr
中使用,查询C++11
相关的文档才知道constexpr
只能包含一个return
语句,这大大限制了constexpr
的功能。好在C++11中不乏一些奇淫技巧,可以在return
里面使用问号表达式或者单if
语句。
constexpr int pow(inr base, int exp) noexcept
{
return (exp == 0 ? 1 : base * pow(base, exp -1));
}
除了上面这个限制外,还有另外一个限制就是只能返回LiteralType,简单来说就是其值可以在编译期知道的,内置类型中除了void
都是LiteralType
类型,自定义类型也可以是LiteralType
类型,这取决于其自定义类型的构造函数是否是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
就是一个LiteralType
类型,但是setX
和setY
却不是constexpr
类型的,这是因为上文中说到的void
类型,这不是一个LiteralType
类型,所以无法设置为constexpr
类型。
constexpr Point p1(9.4, 27.7);
constexpr Point p2(28.8, 5.3);
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);
midpoint
就是一个不折不扣的基于自定义类型的常量函数。一切就是如此的简单,对于上文中提到的void
不是LiteralType
类型,常量函数中只能包含一个return
语句等问题,都在C++14
中得到了解决。总而言之尽可能的去使用constexpr
代替const
,这样会大大提高其适用范围。