constexpr元编程
constexpr变量
通过constexpr定义的变量,通常可以代替使用宏定义的常量,并且能保证类型安全
一般使用constexpr定义一些常量用于指定数组最大长度.使用constexpr修饰的对象,也可以看作是const的
constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf + 1; // mf + 1是常量表达式
constexpr int sz = size(); //之后当size是一个constexpr函数时才是一条正确的声明语句
constexpr size_t MAX_LEN = 32;
int8_t buffer[MAX_LEN];
constexpr与const的区别在于,前者需要保证表达式可在编译时求值,否者会出现编译错误,而后者只是表达变量拥有常量性,若能在编译时求值,也可用于计算上下文,否者将于运行时求值
constinit初始化
C++20中使用constinit定义的变量同样要求在编译时能对表达式求值,但它保留了可变的属性
using namespace std;
consteval int sqr(int n) { return n * n;}
constexpr int res1 = sqr(5);
constinit int res2 = sqr(5);
int main() {
cout << res1 <<endl;
// cout<<++res1<<endl; // error: increment of read-only variable 'res1'
cout << res2 <<endl;
cout<<++res2<<endl;
//constinit auto res3 = sqr(5); // error: 'constinit' can only be applied to a variable with static or thread storage duration
}
constinit解决其实的是全局生命周期变量运行初始化顺序不确定的问题
折叠表达式
折叠表达式时C++17提供的一个强大特性,它简化了一部分需要通过模板递归才能实现的循环功能
折叠表达式拥有如下两大代码形式
1.右折叠:(pack op ...[op init])
2.左折叠:([op init] ...op pack)
它们都需要通过圆括号将表达式括起来,并且都接受一个模板参数报pack,一个二元操作符op和一个可选的初始累计值init,再算上可选的初值后,一共有四种表现形式,而左右折叠的区别在于迭代的方向以及二元操作符的顺序
template<size_t ...Is> //右折叠
constexpr int rsum = (Is + ... + 0);
template<size_t ...Is> //左折叠
constexpr int lsum = (0 + ... + Is);
//(1 + (2 + (3 + (4 + (5 + 0)))))
static_assert(rsum<1,2,3,4,5> == 15);
//(((((0 + 1) + 2) + 3) + 4) + 5)
static_assert(lsum<1,2,3,4,5> == 15);
template<size_t ...Is> //右折叠
constexpr int rsub = (Is - ... - 0);
template<size_t ...Is> //左折叠
constexpr int lsub = (0 - ... - Is);
static_assert(rsub<1,2,3,4,5> == 3);
static_assert(lsub<1,2,3,4,5> == -15);
constexpr函数
若使用constexpr关键字修饰一个函数,表示它可能在编译时求值,只要给出的参数合适,那么在需要常量表达式的地方,例如数组下标,模板参数,静态断言,初始化constexpr常量,编译器将会求值
使用constexpr修饰的函数声明意味着它是inline的
函数返回类型及所有形参的类型都是字面值类型,而且函数体中必须有且只有一条return语句
constexpr int fun(int a){ return a*10; }
若a是常量表达式,fun(a)就是常量表达式。若a不是常量表达式,fun(a)就是变成普通函数。
当constexpr函数是常量表达式时可以用来初始化constexpr变量。
consteval
使用constexpr修饰仅表达一个函数是否可能被编译时求值,而使用consteval修饰时要求函数必须能够被编译时求值
编译内存分配情况
C++20一些函数已经能够在编译时进行内存分配,例如标准库中的vector,string容器也能在constexpr函数中使用,而在此之前只有数组能够被使用
//埃氏筛求素数
consteval vector<int>sievePrime(int n){
vector<bool>marked(n + 1 , true);
for(int p = 2;p * p <= n ; p ++){
if(marked[p]){
for(int i = p * p ; i <= n ; i ++){
marked[i] = false;
}
}
}
}
constexpr的编译速度比传统的模板元编程快很多
编译时虚函数
由于constexpr可以在编译时进行内存分配,那么虚函数也有了应用条件
以多形态求面积为例,编译时只需添加constexpr修饰即可实现编译时多态
struct Shape{//定义接口,无需constexpr修饰
virtual ~shape() = default;
virtual double getArea() const = 0;
}
struct Circle : shape{ //实现接口,用constexpr重写
constexpr Circle(double r):r_(r){}
constexpr double getArea()const override{
return numbers::pi * r_ * r_;
}
private:
double r_;
};
即便虚接口被constexpr修饰,它也能够被非constexpr派生类所重写,而若一个派生类想能够被编译时使用,那么它所实现的函数需要满足并修饰成constexpr的
consteval double testSubtype(){
array<Shape* , 2>shapes{
new Circle(10),
new Circle(5)
};
double sum = 0.0;
for(auto s : shapes){
sum += s->getArea();
delete s;
}
return sum;
}
上述代码在编译时创建Circle对象,并由容器所储存,它在遍历过程中求面颊将会表现多态性,并即使释放创建的对象内存
需要手动释放内存的原因是make_unique等智能指针在C++20中暂时无法在编译时使用
is_constant_evaluated
is_constant_evaluated元函数是由C++20标准库<type_staits>提供的,它能帮助程序员判断一个表达式是否能在编译时执行,主要用途是让程序员可以根据编译时与运行时的不同情况选择不同的实现
constexpr double power(double b , int x){
if(std::is_constant_evaluated()){/*编译时实现*/}
else {/*运行时实现*/}
}
//使用编译时实现分支
constexpr double kilo = pow(10.0 , 3);
//使用运行时实现分支
int n = 3; //非常左值
double mucho = pow(10.0 , n);
上述表达式power(10.0 , n)未处于编译时计算上下文的状态,因为无法在编译时将非常左值转换成右值.
对于is_constant_evaluated而言,编译器首先尝试在编译时求值,若求值成功它将返回真;若无法求值将返回假
非类型模板参数
C++20对非类型模板参数放松约束,曾经的非类型模板参数仅支持简单的数值类型
template<size_t N>constexpr void f(){/*...*/}
在C++20中进一步支持以浮点数作为非参数类型,使用auto占位符表达编译器类型推导的非类型参数.以及用户定义的字面类型
字面类型要求能够在编译器构造对象,并且拥有所有非静态成员都需要public的字面类型,它们和字符串常量,标量一样不可修改
struct Foo{};
template<auto ...>struct ValueList{};
ValueList<'C',0,2L,nullptr,Foo{}>x;
非类型模板参数对字符串的支持仍需要做些额外的工作,而不能简单地使用const char* 作为模板参数,这是因为字符串常量"hello"和其他内置类型不一样,它们拥有自己地地址,对于同一个字符串常量,它的指针const char* 比较结果可能不一样
template <const char* p>struct C{};
C<"hello">c;//编译错误
可以为字符串常量分配确定地地址,使它们可以被模板参数所接受
static const char hello[] = "hello";
static_assert(is_same_v<C<hello> , C<hello>>);//相等
C++20的用户自定义字面类型简化这种场景,我们可以简单地自定义字面类型FixedString
template<size_t N>
struct FixedString{
char str[N];
//将临时地字符串复制到成员中
constexpr FixedString(const char (&s)[N]){copy_n(s , N , str);}
};
//使用自定义字面类型
template<FixedString str>struct C{};
static_assert(is_same_v<C<"hello"> , C<"hello">>);
static_assert(!is_same_v<C<"hello"> , C<"world">>);
字符串入预期地被传递到模板参数中,值得注意的是模板类FixedString通过类模板参数推导规则推导出长度N,然后触发类地构造函数,最后将字符串常量构造成对象进行传递
同样的类型和同样的值表明同一个字面对象,这意味着同一个面向对象程序中仅有一个实例
template<FixedString str>struct C{static constexpr auto ptr = &str;};
template<FixedString str>struct C2{static constexpr auto ptr = &str};
static_assert(!is_same_v<C<"hello"> , C2<"hello">>);//不同类型
static_assert(C<"hello">::ptr == C2<"hello">::ptr);//但同一个字面对象
显然C<“hello”>和C2<“hello”>是不同的类型,但是对于同一个FixedString对象,它们在不同的模板实例中却拥有相同的地址
倘若模板参数使用auto表达非类型函数,交由编译器推导类型,那么字符串常量类型将被推导成const char *,同样地这是非法的,我们希望它被推导出FixedString类型
ValueList<"hello">v1;//编译错误
C++11提供了用户自定义字面量特性,用户为字面量自定义后缀操作符,如此便能转换成字面类型对象
下面代码中,我们以_fs后缀修饰字符串字面量它将被转换成所需要地FixedString对象
template<FixedString str>//用户自定义字面量
constexpr decltype(str)operator""_fs(){return str;}
ValueList<"hello"_fs>v1;//OK
表达式"hello" _fs将触发operator"" _fs<“hello”>()函数调用,它将简单地构造字面对象FixedString并返回
参考资料:<<C++20高级编程>>