谈谈 C++ 的编译期计算

const 变量

  const修饰变量,表示这个变量是不可修改,const变量必须初始化,一经初始化就不可修改:

  • 编译时初始化。
  • 运行时初始化。

  在编译时,可以使用编译时常量来初始化const变量:

1
const int SIZE = 100;

  那么,由于SIZE的值是在编译时就已经确定的,编译器会使用常量 100 来替代程序中出现的SIZE

  另一方面,const变量也可以运行时才初始化:

1
2
vector<int> v;
const int i = v.size();

constexpr 变量

  const变量的值可以在编译时或运行时确定,与const相比,constexpr的限制更多,因为constexpr变量的值必须在编译时就能确定。
  在一些场合之下,变量的值要求是编译期就必须确定的,constexpr变量正好满足要求:

  • 数组的大小必须是编译期常量。
  • std::array的大小必须是编译期常量。
  • std::bitset的大小必须是编译期常量。
1
2
const auto SIZE = 100;
std::array<int, SIZE> arr;

constexpr 函数

  constexpr函数则与编译期计算有关,要是constexpr函数所使用的变量其值能够在编译时就确定,那么constexpr函数就能在编译时执行计算。另一方面,要是constexpr函数所使用的变量其值只能在运行时确定,那么constexpr就和一般的函数没区别。

  C++11 要求constexpr函数不能多于一条语句,但是碰到 if-else 语句时,但可以巧妙地使用条件操作符来替代:

1
2
3
4
constexpr unsigned long long fib( unsigned n ) noexcept
{
return ( n == 0 || n == 1 ? n : fib( n - 1 ) + fib( n - 2 ) );
}

  C++14 中则放松了这个要求:

1
2
3
4
5
6
7
8
9
10
11
constexpr unsigned long long fib( unsigned n ) noexcept
{
if( n == 0 || n == 1 )
{
return n;
}
else
{
return fib( n - 1 ) + fib( n - 2 );
}
}

  要是我们传递一个编译时常量给fib(),那么fib()在程序编译的时候就已经执行好了。代价是增加编译时间,但程序能执行得更高效。


  但是,定义constexpr变量的时候,变量的类型只能是基本数据类型、指针和引用,而不能是其它类型。

1
2
// error: constexpr variable cannot have non-literal type
constexpr string str = "hello";

  但是,我们自己定义的类型却没有这个限制,因为 constructor 和成员函数可以是constexpr函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point
{
public:
constexpr Point( double x = 0, double y = 0 )
: x_( x ), y_( y )
{ }
constexpr double x() const noexcept { return x_; }
constexpr double y() const noexcept { return y_; }
void set_x( double x ) noexcept { x_ = x; }
void set_y( double y ) noexcept { y_ = y; }
private:
double x_, y_;
};

  要是我们使用编译期常量来初始化Point对象,那么,在编译的时候编译器就已经创建了这个对象:

1
constexpr Point pt( 10, 20 ); // Evaluate at compiling time

  另一方面,注意到成员函数也可以是constexpr的,也就是说:

1
2
3
4
5
6
7
8
9
constexpr Point addTwoPoint( const Point &pt1, const Point &pt2 )
{
return { pt1.x() + pt2.x(), pt1.y() + pt2.y() };
}
// ...
constexpr Point pt1( 10, 20 );
constexpr Point pt2( 20, 10 );
constexpr auto add = addTwoPoint( pt1, pt2 );

  要是pt1pt2constexpr变量,那么,addTwoPoint()这个函数会在编译时就执行。

  在 C++11 中,constexpr函数隐式地是const函数,所以你会发现set_x()set_y()这两个函数不能是constexpr函数。

  但在 C++14 中,这个限制放宽了,也就是说这两个函数可以声明为constexpr函数:

1
2
3
4
5
6
7
8
9
class Point
{
public:
// ...
constexpr void set_x( double x ) noexcept { x_ = x; }
constexpr void set_y( double y ) noexcept { y_ = y; }
// ...
};

  这就是说,要是Point对象是constexpr的,甚至在编译时可以修改它的数据成员:

1
2
3
4
5
6
7
8
constexpr Point doublePoint( const Point &pt )
{
return { pt.x() * 2, pt.y() * 2 };
}
// ...
constexpr Point pt( 10, 20 );
constexpr auto p = doublePoint( pt );

参考资料