学习地址: http://c.biancheng.net/view/3730.html
10.constexpr:验证是否为常量表达式
- 常量表达式: 指的就是由多个(≥1)常量组成的表达式,换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式。这也意味着,常量表达式一旦确定,其值将无法修改。
- 实际开发中,我们经常会用到常量表达式。以定义数组为例,数组的长度就必须是一个常量表达式。
void testConstexpr() {
// 1)
int url[10];//正确
// 2)
int url1[6 + 4];//正确
// 3)
// int length = 6;
// int url[length];//错误,length是变量
}
- 我们知道,C++ 程序的执行过程大致要经历编译、链接、运行这 3 个阶段。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。
- 对于用 C++ 编写的程序,性能往往是永恒的追求。那么在实际开发中,如何才能判定一个表达式是否为常量表达式,进而获得在编译阶段即可执行的“特权”呢?除了人为判定外,C++11 标准还提供有 constexpr 关键字。
- constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。
- 注意⚠️,获得在编译阶段计算出结果的能力,并不代表 constexpr 修饰的表达式一定会在程序编译阶段被执行,具体的计算时机还是编译器说了算。
constexpr修饰普通变量:
- C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。值得一提的是,使用 constexpr 修改普通变量时,变量必须经过初始化且初始值必须是一个常量表达式。举个例子:
void testConstexpr1() {
constexpr int num = 1+2+3;
int url[num] = {1,2,3,4,5,6};
cout << "url[0] = " << url[0];
}
- 有读者可能发现,将此示例程序中的 constexpr 用 const 关键字替换也可以正常执行,这是因为 num 的定义同时满足“num 是 const 常量且使用常量表达式为其初始化”这 2 个条件,由此编译器会认定 num 是一个常量表达式。注意,const 和 constexpr 并不相同.
constexpr修饰函数:
- constexpr 还可以用于修饰函数的返回值,这样的函数又称为“常量表达式函数”。
- 注意,constexpr 并非可以修改任意函数的返回值。换句话说,一个函数要想成为常量表达式函数,必须满足如下 4 个条件。
- 整个函数的函数体中,除了可以包含 using 指令、typedef 语句以及 static_assert 断言外,只能包含一条 return 返回语句。
constexpr int display(int x) {
int ret = 1 + 2 + x;
return ret;
}
注意,这个函数是无法通过编译的,因为该函数的返回值用 constexpr 修饰,但函数内部包含多条语句。
如下是正确的定义 display() 常量表达式函数的写法:
constexpr int testConstexpr3(int x) {
//可以添加 using 执行、typedef 语句以及 static_assert 断言
return 1 + 2 + x;
}
- 该函数必须有返回值,即函数的返回值类型不能是 void。原因很简单,因为通过类似的函数根本无法获得一个常量。
- 函数在使用之前,必须有对应的定义语句。我们知道,函数的使用分为“声明”和“定义”两部分,普通的函数调用只需要提前写好该函数的声明部分即可(函数的定义部分可以放在调用位置之后甚至其它文件中),但常量表达式函数在使用前,必须要有该函数的定义。
- return 返回的表达式必须是常量表达式,举个例子:
int num = 3;
constexpr int testConstexpr4(int x){
return num + x;
}
不行
- ⚠️常量表达式函数的返回值必须是常量表达式的原因很简单,如果想在程序编译阶段获得某个函数返回的常量,则该函数的 return 语句中就不能包含程序运行阶段才能确定值的变量。
constexpr修饰类的构造函数:
- 对于 C++ 内置类型的数据,可以直接用 constexpr 修饰,但如果是自定义的数据类型(用 struct 或者 class 实现),直接用 constexpr 修饰是不行的。举个例子
constexpr struct myType {
const char* name;
int age;
//其它结构体成员
};
//报错: Struct cannot be marked constexpr
void testConstexpr5() {
constexpr struct myType mt{"a", 1};
cout << mt.name << " " << mt.age << endl;
}
- 当我们想自定义一个可产生常量的类型时,正确的做法是在该类型的内部添加一个常量构造函数。
struct myType {
constexpr myType(char *name, const int age):name_(name), age_(age){}
char* name_;
int age_;
};
void testConstexpr5() {
constexpr struct myType mt{"a", 1};
cout << mt.name_ << " " << mt.age_ << endl;
//mt.name_ = "hh"; //报错: Cannot assign to variable 'mt' with const-qualified type 'const struct myType'
}
-
可以看到,在 myType 结构体中自定义有一个构造函数,借助此函数,用 constexpr 修饰的 myType 类型的 my 常量即可通过编译。
-
注意⚠️,constexpr 修饰类的构造函数时,要求该构造函数的函数体必须为空,且采用初始化列表的方式为各个成员赋值时,必须使用常量表达式。
-
前面提到,constexpr 可用于修饰函数,而类中的成员方法完全可以看做是“位于类这个命名空间中的函数”,所以 constexpr 也可以修饰类中的成员函数,只不过此函数必须满足前面提到的 4 个条件。
class myType1 {
public:
constexpr myType1(const char *name, int age) :name_(name), age_(age){}
constexpr int getage() const {
return age_;
}
// ⚠️ 这里和网站上的有点不一样,getage必须要在末端加入const , 因为this是const,不加会报错'this' argument to member function 'getage' has type 'const struct myType1', but function is not marked const
private:
const char* name_;
int age_;
};
void testConstexpr6() {
constexpr struct myType1 mt{"a", 1};
cout << " age = " << mt.getage() << endl;
//mt.name_ = "hh"; //报错: Cannot assign to variable 'mt' with const-qualified type 'const struct myType'
}
- 注意,C++11 标准中,不支持用 constexpr 修饰带有 virtual 的成员方法。
constexpr修饰模板函数:
- C++11 语法中,constexpr 可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。针对这种情况下,C++11 标准规定,如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数。
struct myType2 {
char* name_;
int age_;
};
template<typename T>
constexpr T display3(T t) {
return t;
}
void testConstexpr7() {
struct myType2 stu{"hh", 1};
//1. 非常量表达式函数, 因为t不是一个常量
display3(stu);
//2. 是
display3(1);
}
11.constexpr和const区别
- const 用于为修饰的变量添加“只读”属性, 而 constexpr 关键字则用于指明其后是一个常量(或者常量表达式),编译器在编译程序时可以顺带将其结果计算出来,而无需等到程序运行阶段,这样的优化极大地提高了程序的执行效率。
- C++ 11标准中,为了解决 const 关键字的双重语义问题,保留了 const 表示“只读”的语义,
而将“常量”的语义划分给了新添加的 constexpr 关键字。
因此 C++11 标准中,建议将 const 和 constexpr 的功能区分开, - 即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。
结论
相同点:const和consexpr都是用来定义常量的。
不同点:const声明的常量,初始值引用的对象不一定是一个常量;constexpr声明的常量,初始值一定是常量表达式,并且在编译过程就能计算得到结果。
- 还有一个const修饰指针,分为:
- 顶层const指针自身无法修改
- 底层const指针可以修改
int i = 10;
int *const p1 = &i; // 底层const:不能修改p1的指向
const int *p2 = &i; // 顶层const:不能修改p2的值
const int *const p3 = &i; // 底层+顶层const
const int &r = i; // 顶层const:不能通过r修改i值
- constexpr修饰指针,仅对指针有效,与指针所指对象无关
// j的定义必须放在函数体外
int j = 30;
// 函数体内
constexpr int *p1 = &j; // 等价于 int constexpr *p1 = &j;
*p1 = 40; // 正确
p1 = nullptr; // 错误,constexpr指针无法修改