一、const常见的几个关键字和说明符
const这个关键字,如果学C/C++的没用到过,那只能说没学过或者说和没学过一样。其实等理解一门语言到了一定程度,就知道和现实生活一样,到处都是限制条件。如果不按限制条件来,用c++的文档的话说:“不可预知的结果”或者“行为结果未定义”。可是c++的一个非常有名的特点就是灵活,这种灵活是c++的灵魂也是它的致命之伤。
因此,在c++的新标准中,不断的引入一些“交通规则”,试图把开发人员引进一条有规则管理的大道。当然,这种事情不可能在一朝一夕完成,但方向应该是不会改变的。
这其中,const的关键字又引入籽几个类似的关键字和说明符,到目前为止,可以明确的是:const, constinit, consteval, constexpr。
本文将对这几个关键字分别进行展开说明分析。
二、应用说明不同
const和volatile 一起在c++中被称为cv限定符,下面看一下它的基本应用方式:
1、const关键字
const是一种类型说明符,它的应用基本在三个方面上,即变量(普通变量,类实例,指针等)、函数(常函数、函数返回常量值和参数限制、Lambda表达式等)、类成员修饰(前面两种的用法),需要注意的是,在函数的重载(overload)中,const是可以作为控制受限的,换句话说,可以通过其进行函数重载。
常函数只能访问常量(有的编译器允许访问变量,但不可改变值)即不可改变变量的值。常对象只能访问对象的常量,同常函数一样,不允许改变对象的值。
一般来说,c++从早期标准就有这个const.
2、constinit关键字
在前面讨论过DLL中参数变量初始化顺序导致程序随机崩溃的现象,其实在c++编程中,变量的赋值可以静态初始化(简单理解成直接赋值),也可以动态初始化(就是值依赖于其它函数或者外部文件的变量)。前者一般在编译期就可以推导出来,而后者一般需要在运行时才能得出。它就是造成上面所说的随机崩溃的原因,即“static initialization order fiasco”。
constinit有几个特点:首先,它是一种非类型说明符(non-type specifier)主要用做断言(assert);其次确保变量静态初始化;再次只能用于静态(全局)或者thread_local变量使用;最后,它不能constexpr、 consteval关键字一起出现。
constinit其实就是保证编译期完成初始化,这在元编程中可以用到。而且,其用来修饰的对象,只要有constexpr constructor即可,不要求有constexpr destructor。所以 std::shared_ptr可以用constinit,但不能使用constexptr。
constinit只能用于变量修饰,这个关键字是c++20才开始提供的。
3、constexpr说明符
constexptr既可以用于函数声明也可以用于变量声明,它主要用于声明变量值和函数返回值可以在常量表达式中应用。如果是变量,这个变量将在编译期完成,类似于constinit,如果是函数,分为两种情况,一种是函数参数均为常量表达式时,此函数的返回值也是编译期可计算的;否则就退化为普通函数在执行期确定。constexptr从c++11开始,到c++14,c++17直到c++20都有不同情况的扩展,更详细的可以参看:
https://zh.cppreference.com/w/cpp/language/constexpr
它有以个特点,首先它也是一种非类型说明符;其次类型必须是LiteralType(https://zh.cppreference.com/w/cpp/named_req/LiteralType);如果是变量或者符合条件的函数是在编译期初始化;最后其为imply top-level const(只有其自身为const,才能为其它常量表达式使用;另外c++17后可以使用其为内联变量)。
4、consteval说明符
consteval做一种declaration specifier,只能用于对函数声明,表示函数会在编译期进行展开生成常量表达式。它不可应用于析构函数、分配函数或解分配函数。consteval 说明符蕴含 inline,即在同一声明说明符序列中最多只能出现一次。可以理解consteval具有污染性,只要使用了这个声明,与其有调用关系的都必须声明为consteval.
其实可以把consteval和constinit理解成对constexpr的某种意义上的拆解,让const的应用更准确,这样产生歧义的可能性就会非常小。constexpr 和 consteval都可以应用到Lambda表达式中,只要符合它们的要求即可。
通过上面的分析基本可以明白了几种const的应用和不同,另外通过constexpr可以进行编译分支条件判断即constexpr if,也就是说通过constexpr可以在编译期实现类似于if … else的功能。
三、应用场景
下面看一下一个例程:
#include <iostream>
const int a = 6;
const int* p = &a;
class Example
{
public:
int GetData(int x)
{
return x + d_;
}
int GetData(int x) const
{
return x + a;
}
private:
int d_ = 0;
};
class A
{
public:
constexpr A()
{
d_ = 5;
}
//可以不为常量表达式
~A()
{
}
int d_=0;
};
constinit A a1;
//constexpr std::shared_ptr<A> a = std::shared_ptr<A>();//编译错误,从c++20要求const destructable
class B {
public:
constexpr B(int a) {
d_ = a;
}
constexpr int Get() const {
return d_;
}
constexpr ~B() {
d_ = 0;
}
int d_;
};
consteval B Add(B b1, B b2) {
return B(b1.Get() + b2.Get());
}
//consteval
consteval int sqr(int n)
{
return n * n;
}
constexpr int r = sqr(100); // OK
int x = 100;
//int r2 = sqr(x); // 错误:调用不产生常量
consteval int sqrsqr(int n)
{
return sqr(sqr(n)); // 在此点非常量表达式,但是 OK
}
//constexpr int dblsqr(int n)
//{
// return 2 * sqr(n); // 错误:外围函数并非 consteval 且 sqr(n) 不是常量
//}
int main()
{
//常量使用和常函数重载
Example e;
std::cout << "const GetData:" << e.GetData(1) << std::endl;
const Example ce;
std::cout << "const GetData:" << ce.GetData(1) << std::endl;
//constinit的应用
constinit static A a;
std::cout << "constinit get:"<< a.d_ << std::endl;
//constexptr使用
constexpr B b1(5);
constexpr B b2(6);
constexpr int sum = Add(b1, b2).Get();
std::cout <<"constexpr add:" << sum << std::endl;
//consteval使用
constexpr int n2 = sqrsqr(2);
constinit static int nn = sqrsqr(3);
std::cout << "sqrsqr value:" << n2 <<" "<<nn << std::endl;
}
上面的代码非常简单,但也基本上把相关的const的应用说明了。其实如果写的更详细一些,会把从const开始的应用说的更清楚明白,但今天的分析,重点不再于那些细节,那些细节都可以去相关书籍资料上查询。这里重点需要说明的是这几个标识符或者说关键字在元编程中应用的意义,也就是说在编译期展开,这才是重点。
四、总结
从C++17以后,constexpr if其实对元编程的支持使得编译展开形式更为简单明了,此处对其只是简单说明,以后做一篇文章专门来分析这种应用。const随着c++标准的不断的变化,其实就是c++对自身一种完善的过程,从细微入手,慢慢的向上观察,就会发现,c++的进步还是非常大的。
“莫在浮沙筑高台”,基础的重要性,不言而喻,共勉之!