C++17 constexpr的改进
1000 Words | Read in about 5 Min | View times
Overview
C++17的constexpr
扩展了使用范畴,lambda表达式被纳入了constexpr
的表达范畴,弥补了C++17之前无法使用constexpr
lambda的遗憾,我们可以在只接受编译器常量的地方调用lambda表达式定义的lambda函数了。另外还提供了constexpr if
语法的支持,可以在编译期进行逻辑判定,提供编译效率。本节内容通过回顾C++11和C++14标准定义的constexpr
,然后展开介绍C++17对此的改进。
本系列文章将包括以下领域:
本章其他内容请见 《现代C++》
C++11 constexpr
C++11首次引入了constexpr
,用来表达常量表达式,使得我们可以定义编译期常量,一般配合static_assert
来判断编译期常量是否定义成功:
1constexpr int size = 10;
2
3int main() {
4 static_assert(size > 0, "");
5 int arr[size] = { 0 };
6 return 0;
7}
我们还可以使用constexpr
来定义可在编译期求值的常量表达式:
1#include <iostream>
2
3constexpr int fib(int n) {
4 return n < 2 ? n : fib(n - 1) + fib(n - 2);
5}
6
7int main() {
8 static_assert(fib(5) == 5, ""); //编译期求值
9
10 int n = 0;
11 std::cin >> n;
12 std::cout<< fib(n) << std::endl; //运行时求值
13
14 return 0;
15}
在C++11中,constexpr
函数只能有一个return
语句,不支持多个独立语句,最多也只能通过逗号表达式或三目表达式来表达。
constexpr
函数的实参可以是编译期常量,也可以是运行期数值。如果实参是编译期常量,则求值过程在编译期进行,否则在运行期进行。
C++11还支持将类的构造函数和成员函数声明为constexpr
,这样就可以在编译期完成对象构造:
1#include <math.h>
2
3class Circle {
4public:
5 constexpr Circle(double r) : radius(r) { }
6 constexpr double Area() {
7 return M_PI * radius * radius;
8 }
9private:
10 double radius;
11};
12
13int main() {
14 static_assert(Circle(2.0).Area() > 20, ""); //error: static_assert failed due to requirement 'Circle(2.).Area() > 20' ""
15 return 0;
16}
以上例子表明在编译期确实构造了对象,并且在编译期调用了函数。
C++14 constexpr
我们在《C++14更多新特性》这篇文章介绍C++14新特性的时候提到过,constexpr
在C++14的限制得到放宽:
而在C++14中,
constexpr
关键字修饰函数不再要求所有逻辑必须在一个return
语句中完成,可以使用局部变量和循环等
所以,从C++14开始,constexpr
函数可以使用多行语句编写,如上面的fib
函数的例子就可以用循环来改写:
1#include <iostream>
2
3constexpr int fib(int n) {
4 int a = 0, b = 1;
5 for (int i = 0; i < n; ++i) {
6 int c = a + b;
7 a = b;
8 b = c;
9 }
10 return a;
11}
12
13int main() {
14 static_assert(fib(5) == 5, ""); //编译期求值
15
16 int n = 0;
17 std::cin >> n;
18 std::cout<< fib(n) << std::endl; //运行时求值
19 return 0;
20}
然而C++14标准在制定时并未将lambda表达式纳入constexpr
表达范围之内,lambda表达式无法在编译期求值:
1constexpr int foo() {
2 auto lambda = []() { //error: variable of non-literal type '(lambda at main.cpp:19:19)' cannot be defined in a constexpr function
3 return 5;
4 };
5 return lambda();
6}
7
8int main() {
9 static_assert(foo() == 5, "");
10}
不过这是有替代方案的,lambda函数在本质上会被编译器改写为仿函数,仿函数本质也是由类来实现的。前面我们还提到,C++11支持将类的构造函数和成员函数定义成constexpr
,因此我们可以通过定义constexpr
的仿函数来间接实现constexpr
的lambda表达式:
1constexpr int foo() {
2 class Functor {
3 public:
4 constexpr int operator()() {
5 return 5;
6 }
7 };
8 return Functor()();
9}
10
11int main() {
12 static_assert(foo() == 5, "");
13}
C++17 constexpr
constexpr lambda
C++17对constexpr
的适用范围再次进行扩展,已经可以运用在lambda
表达式上了。我们曾经在关于闭包的文章中讲过,编译器在处理lambda表达式的时候,会自动为其合成一个闭包类型(仿函数),这个闭包类型包含一个operator()
的重载操作符,lambda表达式的捕获列表将转换成该仿函数的成员变量,最后lambda表达式会被这个闭包类型的对象替换。
以下面这个例子为示范:
1#include <iostream>
2#include <functional>
3
4std::function<int(int)> foo(int a, int b) {
5 return [a, b](int x) {
6 return a * x + b;
7 };
8}
9
10int main() {
11 std::cout << foo(1, 2)(2) << std::endl; //4
12}
编译器会将其转换为如下形式:
1#include <iostream>
2#include <functional>
3
4class SomeFunctor {
5public:
6 SomeFunctor(int a, int b)
7 : m_a(a), m_b(b)
8 { }
9
10 int operator()(int x) {
11 return m_a * x + m_b;
12 }
13
14private:
15 int m_a;
16 int m_b;
17};
18
19std::function<int(int)> foo(int a, int b) {
20 return SomeFunctor(a, b);
21}
22
23int main() {
24 std::cout << foo(1, 2)(2) << std::endl; //4
25}
C++17对lambda表达式做了如下改进:
-
若lambda表达式捕获的变量是字面量类型(literal type),则整个lambda表达式也将表现为字面量类型。
-
增加
constexpr
lambda表达式语法。 -
若一个lambda表达式满足
constexpr
函数的要求,即使没有明确声明为constexpr
,编译器也会将其推导为constexpr
lambda表达式。
constexpr
lambda表达式的显式语法如下:
1[capture_list] (params_list) constexpr -> return_type {function_body}
在完整体的lambda表达式中,constexpr
添加到参数列表和返回值类型之间,举个例子:
1void foo() {
2 auto fib = [](int n) constexpr -> int {
3 int a = 0, b = 1;
4 for (int i = 0; i < n; ++i) {
5 int c = a + b;
6 a = b;
7 b = c;
8 }
9 return a;
10 };
11
12 static_assert(fib(5) == 5, "");
13}
constexpr
lambda表达式和constexpr
函数一样,需要满足以下条件:
-
返回值的类型必须是字面量类型
-
参数类型必须是字面量类型
-
函数体还需满足:
-
没有使用
inline
语句 -
没有使用
goto
或label -
没有使用
try...catch
语句 -
没有声明或使用非字面量类型的变量或使用thread local storage
-
只要满足上述提到的条件,即使没有将lambda声明为constexpr
,编译器也会自动将其视为constexpr
lambda表达式:
1void foo() {
2 auto fib = [](int n) -> int {
3 int a = 0, b = 1;
4 for (int i = 0; i < n; ++i) {
5 int c = a + b;
6 a = b;
7 b = c;
8 }
9 return a;
10 };
11
12 static_assert(fib(5) == 5, "");
13}
与constexpr
函数一样,constexpr
lambda表达式中,如果参数或捕获列表的参数不是编译期常量,则constexpr
lambda表达式会退化为普通的lambda表达式:
1#include <iostream>
2
3int main() {
4 int x = 0;
5 std::cin >> x;
6
7 auto f = [x](int a, int b) constexpr {
8 return a * x + b;
9 };
10
11 std::cout << f(2, 3) << std::endl; //虽然f声明为constexpr,但由于x是运行期变量,故f退化为普通lambda
12
13 constexpr auto f2 = [x]() { return x; }; //error: constexpr variable 'f2' must be initialized by a constant expression
14
15 return 0;
16}
另外,如果编译器在对constexpr
lambda表达式对编译期求值过程中,遇到throw
或new
语句就会造成编译错误,因为这是需要在运行期才能获得的信息,因此编译期自然会报错:
1int main() {
2 auto f = [](int a) constexpr -> const char* {
3 if (a > 0)
4 return new char[a];
5 throw a;
6 };
7
8 static_assert(f(0) != nullptr, ""); //complie error
9 static_assert(f(1) != nullptr, ""); //complie error
10
11 return 0;
12}
但是只要constexpr
lambda表达式在编译期求值时没有经过throw
或new
语句,就不会产生编译器错误:
1int main() {
2 auto f = [](int a) constexpr -> int {
3 if (a == 0)
4 throw a;
5 return a;
6 };
7
8 static_assert(f(1) == 1, ""); //ok
9
10 return 0;
11}
当一个lambda表达式被显式或隐式地声明为constexpr
,则它可以被转换为一个constexpr
的函数指针:
1auto incr = [](int n) {
2 return n + 1;
3};
4
5constexpr int(*f)(int) = incr;
constexpr if
传统的if-else
语句是在运行期进行判断和选择的,无法运用在编译期,所以在泛型编程中,无法使用if-else
来直接做一些判断。在C++17之前,只能被拆分为一个泛型版本和一个特化版本:
1//C++17之前的写法
2void print()
3{
4 std::cout << std::endl;
5}
6
7template <typename T, typename... Ts>
8void print(T head, Ts... tail)
9{
10 std::cout << head << std::endl;
11 print(tail...);
12}
C++17引入了constexpr if
,支持在编译期进行判断,即在编译期就可以直接知道结果,可以广泛应用于泛型编程。以上例子使用C++17实现则可以将两步合二为一:
1template <typename T, typename... Ts>
2void print(T head, Ts... tail)
3{
4 std::cout << head << std::endl;
5 if constexpr (sizeof...(tail) > 0)
6 print(tail...);
7}
上面的写法还可以通过折叠表达式进一步简化:
1template<typename... T>
2void print(T... t) {
3 ((std::cout << t << std::endl), ...);
4}
再有,constexpr if
在泛型编程中的一个应用是,可以替代冗长的std::enable_if
的写法。在C++17之前,需要为了不同类型条件去写各式各样的特化模版,利用SFINAE加上std::enable_if
,会写得非常复杂:
1//C++17之前
2template<typename T>
3std::enable_if_t<std::is_integral<T>::value, std::string> to_string(T t) {
4 return std::to_string(t);
5}
6
7template<typename T>
8std::enable_if_t<!std::is_integral<T>::value, std::string> to_string(T t) {
9 return t;
10}
11
12//C++17
13template <typename T>
14auto to_string(T t) {
15 if constexpr(std::is_integral<T>::value)
16 return std::to_string(t);
17 else //此处else不可省略
18 return t;
19}
20
21int main() {
22 std::cout << to_string("ok") << std::endl;
23 std::cout << to_string(123) << std::endl; //如果省略else则会报错:error: 'auto' in return type deduced as 'int' here but deduced as 'std::string' in earlier return statement
24 return 0;
25}
上面的例子中,else
不可省略,否则会产生编译器推导错误,无法编译通过。
编译器在做优化时,甚至会把没有使用的分支省略掉,当然另一个分支必须支持C++语法,而老的C++标准即使使用了if
,另一个分支也会被编译。对于constexpr if
,编译器只会实例化条件通过的子句。由于返回值类型我们可以用auto
让编译器自动推导,所以每个constexpr if
子句的返回值类型也不需要完全一致。