Skip to content

constexpr

关于编译期动作,有必要介绍下constexpr。在这之前有必要简单提一下constexpr与const的关系,两者字面上都表达常量的意思。

主要的区别是:const修饰的变量可以在运行时才初始化,而constexpr则一定会在编译期初始化。

而const表示的是read only的语义,保证修饰的变量运行时不可以更改,如果直接改动它,编译器在编译时会报错。const修饰的变量可以在运行时才初始化,而constexpr则一定会在编译期初始化。

有人可能会用指针等骚操作来修改const修饰的变量值,这种情况下,CPP标准规定产生的是未定义行为,具体可能不同编译器的具体行为会不相同。所以骚操作魔改const后,无论产生什么行为,都没必要奇怪,也没必要深究。

下面具体介绍下constexpr。

如上所述,constexpr修饰的才是真正的常量,它会在编译期间计算出来,整个运行过程中都不可被改变。constexpr还可用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来,然后作为一个常量,但是如果编译期间不能被计算出,此函数就是被当作一个普通函数处理。

如何使用constexpr?这里我直接贴出cppreference中的示例代码:

cpp
#include <iostream>
#include <stdexcept>
// C++ 11 constexpr functions use recursion rather than iteration
// (C++ 14 constexpr functions may use local variables and loops)
constexpr int factorial(int n) { return n <= 1 ? 1 : (n factorial(n - 1)); }

// literal class
class conststr {
  const char p;
  std::size_t sz;
public:
  template <std::size_t N>
  constexpr conststr(const char (&a)[N]) : p(a), sz(N - 1) {}
  // constexpr functions signal errors by throwing exceptions
  // in C++ 11, they must do so from the conditional operator ?:
  constexpr char operator[](std::size_t n) const { return n < sz ? p[n] : throw std::out_of_range(""); }

  constexpr std::size_t size() const { return sz; }
};

// C++ 11 constexpr functions had to put everything in a single return statement
// (C++ 14 doesn't have that requirement)
constexpr std::size_t countlower(conststr s, std::size_t n = 0, std::size_t c = 0) {
  return n == s.size() ? c : 'a' <= s[n] && s[n] <= 'z' ? countlower(s, n + 1, c + 1) :     countlower(s, n + 1, c);
}

// output function that requires a compile-time constant, for testing
template <int n>
struct constN {
  constN() { std::cout << n << '\n'; }
};

int main() {
  std::cout << "4! = ";
  constN<factorial(4)> out1;  // computed at compile time
  volatile int k = 8; // disallow optimization using volatile
  std::cout << k << "! = " << factorial(k) << '\n';  // computed at run time

  std::cout << "the number of lowercase letters in \"Hello, world!\" is ";
  constN<countlower("Hello, world!")> out2;  // implicitly converted to conststr
}

可以大体观察到constexpr的语法如下:

cpp
constexpr literal-type identifier = constant-expression ;
constexpr literal-type identifier { constant-expression } ;
constexpr literal-type identifier ( params ) ;
constexpr ctor ( params ) ;

通过示例代码及相关注释,就可以看到,能在编译期做constexpr就会优先在编译期计算,编译期不行就在运行时计算。也可以看到,在C++ 14之前constexpr修饰函数时不能有if-else for循环等语句,而在C++ 14后,这个问题有了改善。

那什么情况下应该使用constexpr修饰函数?

不在乎编译时间的话,尽可能用constexpr修饰所有的函数,大家有时间可以看看cpp的源码,多数成员函数都是使用的constexpr修饰。

简介

constexpr 是C++ 11新加入的关键字,C++ 11为了提高代码执行效率做了一些改善。这种改善之一就是:生成常量表达式,允许程序利用编译时的计算能力。所谓常量表达式,指的就是由多个(≥1)常量组成的表达式。换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式。这也意味着,常量表达式一旦确定,其值将无法修改。

因此constexpr 关键字不是一个数据类型,它属于一个修饰符,修饰的对象是一个常量,即表示该对象这是一个常量表达式,常量表达式主要是允许一些计算发生在编译时值不会改变并且在编译过程就能得到计算结果的表达式,即发生在代码编译就可以确认的值而不是运行的时候。

对于非常量表达式来说,程序只有在运行的过程中才能计算出结果,但是对于常量表达式只要在编译过程中计算一次就可以了,因此节省了每次运行都要计算一次的时间,constexpr关键字就是指定了常量表达式具有在编译过程中计算结果的功能,这对于程序来说是一个很大的优化。

constexpr用在何处?

修饰变量

constexpr所修饰的变量一定是编译期间可求值的。C++ 11新标准规定,允许将变量声明为constexpr 类型以便由编译器来验证变量的值是否是常量表达式。constexpr 修饰符声明可以在编译时求得函数或变量的值,声明为constexpr的变量一定是一个常量,而且必须用常量表达式来进行初始化。

cpp
constexpr int a = 4; 
//4是常量

constexpr int b = a + 1; 
//a+1是常量

constexpr int c = f(x); 
//f(x)函数的返回值必须是个常量;

int array[b] = {1,2,3,4,5}; 
//由于b是常量,所以可以定义为数组长度

int d = 6; 
//d不是常量表达式

constexpr int e = d;
//e不是常量表达式,因为d只有在运行到的时刻才初始化

当变量b使用的constexpr 用 const 关键字替换也可以正常执行,这是因为b的定义同时满足“b是 const 常量且使用常量表达式为其初始化”这 2 个条件。但是需要注意const对象的值不一定要在编译时就被推断出来,也可以在运行中被初始化,所以值不一定是一个constexpr。因此可以认为const为“运行期常量”,即运行时数据是只读的。而constexpr为“编译期”常量,这是const无法保证的。

另外需要重点提出的是,当常量表达式中包含浮点数时,考虑到程序编译和运行所在的系统环境可能不同,常量表达式在编译阶段和运行阶段计算出的结果精度很可能会受到影响,因此 C++ 11 标准规定,浮点常量表达式在编译阶段计算的精度要至少等于(或者高于)运行阶段计算出的精度。

修饰引用和指针

const修饰引用时,其意义与修饰变量相同。但const在修饰指针时,规则就有些复杂了。const放在号前,表示指针指向的内容不能被修改.const放在号后,表示指针不能被修改;而constexpr关键字只能放在*号前面,并且表示指针本身的内容不能被修改。

cpp
constexpr int* q = nullptr;  
//q是一个指向整数的常量指针

修饰函数

constexpr 函数是指能用于常量表达式的函数。是指可以在编译期间就完全展开并知道返回结果的函数。

cpp
constexpr int add(int x, int y){    return x + y;}// 将在编译时计算
const int val = add(9, 1);

除了编译时计算的性能优化,constexpr的另外一个优势是,它允许函数被应用在以前调用宏的所有场合。define只是简单的替换,没有类型,constexpr可以做到防窜改与类型安全。例如,你想要一个计算数组size的函数,size是10的倍数。如果不用constexpr,你需要创建一个宏或者使用模板,因为你不能用函数的返回值去声明数组的大小。但是用constexpr,你就可以调用一个constexpr函数去声明一个数组。

cpp
constexpr int getDefaultArraySize (int multiplier){    return 10 * multiplier;}
int my_array[ getDefaultArraySize( 3 ) ];
int a = 4;  //非常量表达式
getDefaultArraySize(a);  //ok

constexpr修饰的函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。不过,我们不必因此而写两个版本,所以如果函数体适用于constexpr函数的条件,可以尽量加上constexpr。

C++ 11中在使用constexpr修饰的函数时需要注意

1. 函数体只有单一的return语句

cpp
constexpr int add(int x, int y){    
 int ret =  x + y;  //C++ 11中错误,contexpr函数中有别的语句    
 return ret;
}

constexpr int add(int x, int y){    
 //可以添加 using 执行、typedef 语句以及 static_assert 断言    
 return x + y; //正确,contexpr函数中只有单一return语句
}

这条限制在C++ 14中这条己经不再是限制。


2. 函数体必须有返回值

cpp
constexpr void fun(){    
 //函数体    
 // 返回值类型为 void 的函数,不属于常量表达式函数。    
 //因为通过类似的函数根本无法获得一个常量。
}

C++ 11中要求不能是void函数,但C++ 14己不再限制


3. constexpr函数内部不能调用非常量表达式的函数,会造成编译失败

cpp
int funA(int x){
 return x;
}

constexpr int funB()    //编译失败
{
 int x = funA(5); //调用了非constexpr函数funA!    
	return x;
}

4. return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式

cpp
int num = 3;
constexpr int funA(int x){    
 return num + x;
}

int main(){    
 int a[funA(1)] = { 1,2,3,4 };//编译错误,由于num为变量,所以funA(1)不是常量。    
 return 0;
}

5. constexpr函数在调用时若传入的实参均为编译期己知的,则返回编译期常量。只要任何一个实参在编译期未知,则它的运作与普通函数无异,将产生运行期结果。constexpr函数的构成条件不满足时,就会变成一个普通的函数。因此constexpr函数可以同时应用于编译期语境或运行期语境。

cpp
//constexpr函数(即可当编译期常量表达式函数,也可以当作普通函数使用)
constexpr int func1(int x){    
 return x + 2;
}

constexpr int N = 5; 
int a = 5; 
constexpr int c1 = func1(N);  
//constexpr语境: 返回值赋值constexpr变量,是constexpr函数(实参也为constexpr)。
int c2 = func1(a);            
//非constexpr语境:func1用于返回值赋值给普通变量时,当作普通函数使用!
int c3 = func1(N);            
//非constexpr语境:func1用于返回值赋值给普通变量时,当作普通函数使用!

6. 调用常量表达式函数前,函数定义必须放在调用前。不能仅作函数声明。

cpp
#include <iostream>
using namespace std;

int funA(int x);  
//普通函数可以声明
constexpr int funB(int x); 
//常量表达式函数的声明
constexpr int funB(int x) //常量表达式函数定义必须定在调用前
{    
 return x + 5;
}

void main(){     
 funB(2); //调用常量表达式函数   
 funA(4); //调用普通函数
}

//普通函数的定义可以放在被调用之后
int funA(int x) {    
 return x + 6;
}

总之,constexpr修饰函数的时候,函数要有返回值,但不要求返回值为编译器常量。编译器会检测调用该函数时在编译器能否得到值,如果可以编译器会对它进行优化否则就当做普通函数。所以可以用constexpr的函数就尽量使用。

函数只有足够简单才能在编译时求值。constexpr函数必须不能有循环、没有局部变量。当函数加上了constexpr但没有遵循相应的规则,那么编译时间会变长,有了错误调试难度也会增加。

修饰构造函数

  • 自定义类的构造函数须为constexpr函数。
  • constexpr不能用于修饰virtual函数。因为virtual是运行时的行为,与constexpr的意义冲突。
  • C++ 11中被声明为constexpr成员函数会自动设为const函数,但C++ 14中不再自动设为const函数。
  • 用constexpr构造函数创建的对象也可以当作常量使用。
cpp
//结构体自定义数据类型
struct myStruct {    
    //内部需要添加一个常量构造函数    
    constexpr myStruct (int x, int y):x(y),x(y){};    
    int x;    
    int y;
};
            
constexpr struct myStruct A { 1, 9 };
//利用结构体自定义数据类型定义,使用constexpr修饰
            
//类自定义类型
class Point{    
    double x, y;
public:    
    // 常量表达式构造函数    
    constexpr Point(double x = 0, double y = 0) noexcept : x(x),y(y) {}    
    // 成员函数(constexpr不允许用于修饰virtual函数)    
    constexpr double getX() const noexcept { return x; }    
    constexpr double getY() const noexcept { return y; }    
            
    // 以下两个函数在C++ 11中无法声明为constexpr原因如下:    
    // 1.C++ 11中被声明为constexpr成员函数会自动设为const函数,而这两个函数需要更改成员变量的值,而const成员函数却不允许修改成员的值,会产生编译错误。C++ 14中constexpr函数不再默认为const函数,因此可以修改成员变量。    
    // 2.C++ 11中constexpr函数不能返回void型,但C++ 14中不再限制。    
    constexpr void setX(double newX) noexcept { x = newX; }    
    constexpr void setY(double newY) noexcept { y = newY; }
}; 
            
constexpr Point midpoint(const Point& p1, const Point& p2) noexcept{ 
    //p1和p2均为const对象,会调用const版本的getX()和getY()    
    return { (p1.getX() + p2.getX()) / 2, (p1.getY() + p2.getY()) / 2 };
} 
            
int main(){    
    constexpr Point p1(9.4, 27.7);          // ok, p1为编译期常量    
    constexpr Point p2(28.8, 5.3);          // ok,同上    
    constexpr auto mid = midpoint(p1, p2);  // mid为编译期常量,mid.getX()也是编译期常量    
    return 0;
}

因此,当使用constexpr 修饰构造函数时,构造函数的函数体必须为空,为各个成员赋值时采用初始化列表的方式,赋值为常量表达式。

修饰模板函数

constexpr也可以修饰模板函数,模板函数中不给定具体的数据类型,实例化以后才能给定具体的数据类型,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的,如果实例化后,函数的参数没有满足constexpr函数的要求,那么constexpr就会被忽略,实例化后的函数为普通的函数。

cpp
#include <iostream>
using namespace std;

//模板函数
template<typename T>
constexpr T funA(T x){    
 return x;
}

int main(){    
 int i = 6;                    //i为变量,赋值为6    
 constexpr int a = funA(4);    //此时funA为常量表达式函数    
 int b = funA(i);              //此时funA为普通函数    
 return 0;
}

constexpr和const的区别?

语义的区别

在 C 里面,const 很明确只有“只读”一个语义,不会混淆。C++ 在此基础上增加了“常量”语义,也由 const 关键字来承担。C++ 11以前,const 既表示“常量”,也表示“只读”。“只读”不意味着不能被修改。常变量与常量不等价,常变量是可能被其他途径修改的。凡是表达“常变量”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。

cpp
#include <iostream>
using namespace std;

int main(){   
	int a = 10;   
 const int & con_b = a;   
 cout << con_b << endl;//10   
 a = 20;   
 cout << con_b << endl;//20   
 //可以看到,程序中用 const修饰了con_b变量,表示该变量“只读”,即无法通过变量自身去修改自己的值。   
 //但这并不意味着 con_b 的值不能借助其它变量间接改变,通过改变 a 的值就可以使 con_b 的值发生变化。   

 a = 10;   
 const int *con_c = &a;   
 cout << *con_c << endl;//10   

 a = 20;   
 cout << *con_c << endl;//20   

 //*con_c = 30;error,不能通过con_b的层面改变a的值   
 //只是表明不能通过con_c的层面修改a的值,不代表a的值不可变,const指针可以指向非const的变量。   
 //表示从con_c的层面不会改变a的值,不代表a的值不可变,因为a本身是一个变量,随时可能被改变。
}

C++ 11 把“常量”语义拆出来,交给新引入的 constexpr 关键字,告诉编译器这个变量可以尽情优化。const 修饰属性为只读。

constexpr 关键字修饰的是常量,所谓常量表达式,指的就是由多个(≥1)常量组成的表达式。换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式。这也意味着,常量表达式一旦确定,其值将无法修改。

常量表达式可以在编译阶段就直接计算出来,提高程序运行效率。而非常量表达式只能在程序运行时计算出来。

cpp
//示例1
int x = 10;
const int i = x * x;       //正确
constexpr int j = x * x;   //编译器报错,x*x不是常量表达式
constexpr int j = i;       //错误,i不是常量表达

//示例2
int x = 5;
const int i = x * x;     //正确
constexpr int j = 5;     //正确
int a[i];           //错误,i不是常量表达式
int b[j];           //正确,j是常量表达式

const定义的变量,不要求在编译时就能被算出,也就是说可以由变量赋值。

constexpr定义的变量,在编译时就能被算出,只能由常量表达式赋值。

也就是说constexpr 比 const更常量一点。

const可以说是在 【有了初值之后】,永远只读。

而constexpr则是从 【没初始化的时候】 就已经是一个常量了。

因此 在C++ 11 标准中,建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。

执行初始化阶段区别

由于constexpr 是 C++ 11 引入的,其目的是为了引入更多的编译时计算能力,因此constexpr是一种比const 更严格的束缚, 它修饰的表达式常量本身在编译期间执行初始化,只要在编译期间执行一次,不需要在程序运行期间反复执行。即:

const并未区分编译时常量和运行时常量,只是说明使用时不能不能修改。

constexpr限定为编译时常量。

因此编译器可以在编译时对constexpr的代码进行非常大的优化,比如将用到的constexpr表达式都直接替换成最终结果等。

顶层const和底层const

从 const 指针开始简单说起。对于初学者来说这大概是很难理解的一个知识点,怎么区分这四个呢?直接从右向左读就行了。

cpp
const int* p1; 
//p1是一个指针,指向int常量,表示p1所指对象内容不可以改,所指地址可以改。

int const *p2; 
//p2同p1,写法不同,两者等价。相对来说p1,p2是最常用传参或者返回值的手段。

int* const p3; 
//p3是一个常量,且是个指针,指向int类型,表示p3所指对象内容可以改,所指地址不可以改。

const int * const p4; 
//p4是一个常量,且是个指针,指向int类型常量,表示p4所指对象内容不可以改,且所指地址也不可以改。

因此指针本身是不是常量和指针所指向的对象是不是常量就是两个互相独立的问题。用顶层表示指针本身是个常量,底层表示指针所指向的对象是个常量。

更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用;底层 const 则与指针和引用等复合类型有关,比较特殊的是,指针类型既可以是顶层 const 也可以是底层 const 或者二者兼备。

简单来说const修饰的对象本身不能改变就是顶层const,但如果是指针或者引用的对象不能改变,则称为底层const。

cpp
int i = 0;
const int j = 42;   //j是顶层const,i的本身值不可以改变

int *const p1 = &i; //p1是顶层const,p1的本身值不可以改变

const int *p2 = &j; //p2是底层const,p2本身值可以改变,但所指内容不可以改变

int const *p3 = &j; //p3是底层const,p3本身值可以改变,但所指内容不可以改变

const int *const p4 = p2; //*右边的const是顶层 const,*左边的是底层const

const int &r = i; //所有的引用本身都是顶层 const,因为引用一旦初始化就不能再改为其他对象的引用,这里用于声明引用的 const 都是底层 const

C++中主要还是针对指针类型来说:

顶层 const:指针本身是常量。此时,指针在定义初始化之外,不能被赋值修改。称指针为指针常量。 底层 const:指针指向的变量是常量。此时,不能通过解引用指针的方式,修改变量的值。称指针为常量的指针。

而必须明确一点,在constexpr声明中如果定义了一个指针,限定符conxtexpr仅对指针有效,与指针所指的对象无关。

cpp
const int* p = nullptr;      // p是一个指向整型常量的指针
constexpr int* q = nullptr;  //q是一个指向整数的常量指针

p是一个指向常量的指针,q是一个常量指针,其中的关键在于constexpr把它定义的对象当作了顶层const。所以注意被constexpr所修饰的指针是顶层const。