const
const可以用于定义变量,它的值不能被改变。
const int bufSize = 512;
如果有代码试图修改这个变量,就会发生编译错误
const当前也可以用于修改指针,但是会带来一些混乱,比如:
const double pi = 3.14;
const double *cptr = π //指向常数的指针
*cptr = 4; //编译错误
double var = 45;
double *const pvar = &var; //指向变量为常数
*pvar = 40; //正确
常量表达式
const限定符只能保证初始化之后不会被修改,而变量的值可以在编译阶段确定,也可以在执行时确定。
C++中的常量表达式指的是固定并且在编译过程就能确定计算结果的表达式
比如:
const int max_files = 20; //20是字面值
const int limit = max_files + 1; //max_files为常量表达式,1是字面值。
const int sz = get_size(); //sz不是常量表达式。
上面的表达式同时满足两个条件:
- 变量本身是const类型
- 初始值必须是字面值或者常量表达式
如果变量类型不是const,那么无论它的初始值是不是常量表达式,它就只是一个变量;如果初始值不是常量表达式,那个变量的值就无法在编译期间确定只能在执行时占用CPU开销来赋值
变量的值在编译期间决定,带来的好处是提高效率,例如例子中的limit的表达方式,在照顾可读性的同时,又不会影响执行效率
costexpr
constexpr
:指明变量或者函数的值可以在常量表达式中出现
为什么要引入常量表达式:
- 类型1+2, 3*4这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入程序运行时,将能增加程序的性能
有了const,为什么还要引入constexpr:
- C++98/03具备const类型,不过它对只读内存(ROM)支持的不够好。这是因为在C++中const类型只在初始化后才意味着它的值应该是常量表达式,从而在运行时不能被改变。不过由于初始化依旧是动态的,这对ROM设备来说不适用。这就要求在动态初始化前就将常量计算出来。为此标准增加了constexpr,它让函数和变量可以被编译时的常量取代,而从效果上来说,函数和变量在固定内存设备中要求的空间变得更少,因而对于手持,桌面等用于各种移动控制的小型嵌入式设备的RMO而言,C++11支持的更好
costexpr变量
一般来说,在日益复杂的系统中确定变量的初始值到底是不是常量表达式并不是一件容易的事情。为了解决这个问题C++11允许将变量声明为costexpr类型以便由编译器验证变量的值是否是一个常量表达式
变量声明为constexpr类型,就意味着一方面变量本身是常量,也意味着它必须用常量表达式来初始化:
constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr float y{108};
constexpr int i; // Error! Not initialized
int j = 0;
constexpr int k = j + 1; //Error! j not a constant expression
如果初始值不是常量表达式,就会发生编译错误
costexpr函数
除了能用常量表达式初始化constexpr变量声明为,还可以使用consstexpr函数。它是指能用于常量表达式的函数,也就是它的计算结果可以在编译时确定。。
例如下面的计算阶乘的constexpr函数。
constexpr long long factorial(int n){
return n <= 1? 1 : (n * factorial(n - 1));
}
constexpr long long f18 = factorial(20);
可以用它来初始化constexp变量。所有计算都在编译时完成,比较有趣的是像溢出这样的错误也会在编译期检出
定义的方法就是在返回值类型前加constexpr关键字。但是为了保证计算结果可以在编译期就确定,必须满足以下条件:
- 函数中只能有一条
return
语句
也就是说函数体中只能有一条语句,而且该语句必须是return语句,因此,下面:
constexpr int data(){
const i = 1;
return i;
}
无法通过编译。不过一些不会产生实际代码的语句在常量表达式函数的使用下,可以通过编译,比如说static_assert,using、typedef之类的。因此,下面:
constexpr int f(int x){
static_assert(0 == 0, "assert fail.");
return x;
}
可以通过编译。
- 在使用前必须已经有定义。
看个例子:
constexpr int f();
int a = f();
const int b = f();
constexpr int c = f(); // 无法通过编译
constexpr int f() { return 0;}
constexpr int k = f();
在a和b的定义中,编译器会将f()转换为一个函数调用,而在c的定义中,由于是一个常量表达式的值,因此,会要求编译器进行编译时值计算。这时候由于f还没有定义,就会导致编译错误。
- return返回语句表达式中不能使用非常量表达式的函数、全局数据,而且必须是一个常量表达式。也就是说常量表达式不能使用非常量表达式的函数:
const int f(){
return 1;
}
constexpr int g(){
return f();
}
int g = 3;
constexpr int b(){
return g;
}
不能通过编译,因为return表达式不能包含运行时才能确定的返回值
总结
简单地说,constexpr可以:
- 加强初始值的检查
- 计算的时机从运行期提前到编译期,比宏定义效率更高
costexpr构造函数
聚合类
聚合类允许调用者直接访问其成员,并且具有特殊的初始化形式。聚合类满足下面条件:
- 所有成员都是public
- 没有定义构造函数
- 没有类内初始值
- 没有基类,也没有虚函数
怎么理解呢?
首先,聚合类其实就是一个C结构体;其次,聚合这个词,应该是相对组合的,表明了成员和类之间的松散关系。
当一个类是聚合类时,就可以使用初始值列表向下面这样初始化了。
struct Point{
int x;
int y;
};
Point pt = {10, 10};
字面值常量类
我们知道,constexpr函数的参数和返回值都必须是常量表达式。而常量表达式的最基本要素就是字面值类型。字面值类型除了包括算数类型,引用和指针之外,某些类也属于字面值类型,C++称之为字面值常量类。主要包括两种情况:
- 数据成员都是字面类型的聚合类。
- 虽然不是聚合类,但是只要满足下面的条件,也是字面值常量类:
- 数据成员都必须是字面值类型 ===》 从而可以在编译阶段求值
- 类必须至少含有一个constexpr构造函数 ===》 场景这个类的constexpr类型的对象
- 如果一个数据成员具有类内初始值,则初始值必须是常量表达式;如果成员属于某种类,初始值必须使用该类的constexpr构造函数 ===》保证即使有类内初始化,也可以在编译阶段解决
- 类必须使用析构函数的默认定义 ===》析构函数不能由不能预期的操作
constexpr构造函数
- 通过前置constexpr关键字,就可以声明constexpr构造函数
- 除了声明为=default或者=delete以外,constexpr构造函数的函数体一般为空,使用初始化列表或者其他的constexpr构造函数初始化所有数据成员。
constexpr Point(int _x, int _y)
:x(_x),y(_y){}
constexpr Point()
:Point(0,0){}
int x;
int y;
};
constexpr Point pt = {10, 10};
这样声明以后,就可以在使用constexpr表达式或者constexpr函数的地方使用字面值常量类了。
// constexpr.cpp
// Compile with: cl /EHsc /W4 constexpr.cpp
#include <iostream>
using namespace std;
// Pass by value
constexpr float exp(float x, int n)
{
return n == 0 ? 1 :
n % 2 == 0 ? exp(x * x, n / 2) :
exp(x * x, (n - 1) / 2) * x;
}
// Pass by reference
constexpr float exp2(const float& x, const int& n)
{
return n == 0 ? 1 :
n % 2 == 0 ? exp2(x * x, n / 2) :
exp2(x * x, (n - 1) / 2) * x;
}
// Compile-time computation of array length
template<typename T, int N>
constexpr int length(const T(&)[N])
{
return N;
}
// Recursive constexpr function
constexpr int fac(int n)
{
return n == 1 ? 1 : n * fac(n - 1);
}
// User-defined type
class Foo
{
public:
constexpr explicit Foo(int i) : _i(i) {}
constexpr int GetValue() const
{
return _i;
}
private:
int _i;
};
int main()
{
// foo is const:
constexpr Foo foo(5);
// foo = Foo(6); //Error!
// Compile time:
constexpr float x = exp(5, 3);
constexpr float y { exp(2, 5) };
constexpr int val = foo.GetValue();
constexpr int f5 = fac(5);
const int nums[] { 1, 2, 3, 4 };
const int nums2[length(nums) * 2] { 1, 2, 3, 4, 5, 6, 7, 8 };
// Run time:
cout << "The value of foo is " << foo.GetValue() << endl;
}
if constexpr
我们知道,constexpr
将表达式或函数编译为常量结果。一个很自然的想法是,如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高。C++17支持if constexpr(表达式),允许在代码中声明常量表达式的判断条件,如下:
#include <iostream>
template<typename T>
auto print_type_info(const T & t){
if constexpr (std::is_integral<T>::value){
return t + 1;
}else{
return t + 0.01;
}
}
int main() {
std::cout << print_type_info(5) << "\n";
std::cout << print_type_info(5.5) << "\n";
}