当前位置: 代码迷 >> 综合 >> modern C++ (1)
  详细解决方案

modern C++ (1)

热度:71   发布时间:2023-11-27 19:21:49.0

文章目录

  • 前言
  • constexpr
    • 一个有意思的地方
  • constexpr作用于函数
  • if constexpr


前言

这是一个系列文章。C++是一个随时代发展的语言,大量的语法封装让它显得它有点肥胖,让人望洋兴叹。但是这并不妨碍我们来学习它的新东西,学习C++对效率的极致追求。有些语法或关键字你可能早已熟悉,我想说的是,这个系列文章尽量只讲新东西,换句话说,如果你看见某些熟悉的语法时,不要急着跳过,因为C++有可能将这个语法拓展了新的东西,比如auto在C++14之前不支持推导函数返回值,但是在C++14之后可以这样做,甚至用来做lambda表达式的泛型推导。

constexpr

你可能对const很熟悉,但是你可能不太熟悉这个关键字。C++中有许多的常量表达式(const expression),这些表达式的值是唯一确定的,比如1 + 1,2 * 3, 但是编译器不知道这件事,至少在编译期间不知道。所以在编译器只会在运行时去完成计算,没错,你应该意识到这是可以优化的。constexpr的作用就是将常量表达式从运行期分离出来到编译期间处理,你用constexpr修饰一个对象,这就是在告诉编译器这个对象会产生一个常量表达式,你不必把它放到运行期间计算,而编译器则会遵循这个指令去检查是否为常量表达式,然后提前处理,优化程序。

// constexpr
void test_constexpr1(){
    int i = 2;const int c1 = 2;const int c2 = i; //const修饰的变量可以用变量来初始化,合法const int c3;     //非法,需要给定初值constexpr int ce1 = 1 + 2; //定义一个常量表达式 ce1,合法constexpr int ce2 = c1; //非法constexpr int ce3 = i;  //非法constexpr int ce4;      //非法
}

可以看到constexpr与const的对比,const可以用普通变量来初始化,但是constexpr只能使用常量表达式来初始化,甚至不能使用const变量来修饰。和const类似的是,constexpr修饰的对象必须给定初始化的表达式。有趣的是,constexpr弥补了const变量的某些我们希望看到的作用,比如数组

#define SIZE 5
void test_constexpr2(){
    const int c = 5;constexpr int ce = 5;int arr1[5];int arr2[SIZE];int arr3[c]; //非法int arr4[i]; //可以,i是常量表达式
}

可以看到,constexpr修饰的变量可以用于定义数组,因为数组的定义需要常量表达式,而const给的保证是只读,const修饰的对象有可能是变量,只是不会被修改罢了。constexpr弥补了这个缺陷。

一个有意思的地方

//const
void test_const(){
    const int c = 5;const int* p = &c;//c = 1; 非法,妄图给只读变量赋值*const_cast<int *>(p) = 2;        //合法,去掉const后可以修改,这证明了const修饰的还是变量cout << c << endl;cout << *p << endl;
}

你可能会惊讶于最后的输出是5和2,而不是你想的2和2。具体原因你可以移步这篇博客 。
这里我主要想说明的是,虽然const的意思是常量,但const修饰的不一定是常量表达式,const代表的意思大部分是“只读”,只是带有了部分的常量性。而constexpr另外的作用就是分离出const作为常量的那一部分,以后如果有常量表达式,推荐使用constexpr而非const。

constexpr作用于函数

没错,constexpr也可以作用于函数上。使用的方法就是将constexpr关键字加在函数返回值的前面即可。这表明函数的返回值可能是一个常量表达式。编译器就会在编译期间去验证函数是否返回constexpr,如果是,那么就直接算出结果,不必等到允许时再操作。

constexpr int func1(int i){
     //这表示func的返回值可能是一个constexprreturn i;
}
constexpr int func2(int i){
     //这个函数无论参数是什么,实际上都返回constexprreturn 5; 
}
// 各种test
int i = 5;
int arr1[func1(5)]; //合法
int arr2[func1(i)]; //非法func1(i);   //直接call,合法int arr3[func2(5)]; //合法
int arr4[func2(i)]; //非法func2(i);   //直接call,合法

对于func1的测试我们可以看到,当返回值跟参数有直接关系时,参数为constexpr,那么返回值为constexpr。参数为正常的int变量,那么返回值也是int变量。于是我们想知道一个问题的答案,constexpr修饰的函数的返回值如果跟参数无关呢?func2的结果证明了一件事,constexpr修饰的函数无论返回值跟参数有没有关系,参数必须是constexpr,返回值才有可能是constexpr。而且更有意思的是,constexpr修饰的函数不会构成函数重载,换句话说 ,constexpr修饰的函数既可以是constexpr函数,也可以是普通函数,这是一个有趣的feature。
上面的测试都是针对一个参数的函数,你可能会想,那么多个参数呢?constexpr的返回值要想是constexpr的对象,应该满足什么呢?是所有的参数都是constexpr还是第一个参数是constexpr就行?

// 测试 constexpr修饰的多参数函数
constexpr int func1(int i, int j){
     //返回值只和i有关return i;
}
constexpr int func2(int i, int j){
     //返回值只和j有关return j;
}
constexpr int func3(int i, int j){
     //返回值和i j都有关return i + j;
}
constexpr int func4(int i, int j){
     //返回值和i j 无关return 5;
}//测试int arr1[func1(1, 2)]; //okint arr2[func1(i, 2)]; //wrongint arr3[func1(1, j)]; //wrongint arr4[func1(i, j)]; //wrong

我对后面的3个函数做了下面同样的测试,发现对于多参数的函数,constexpr修饰的函数返回constexpr类型的对象同样的必须保证所有的参数都是constexpr,无论返回值是否和参数有直接关系。
几点要注意的是: constexpr同样可以修饰返回值为void的函数。constexpr修饰的函数可以使用递归,从C++14开始,constexpr修饰的函数才允许在内部定义局部变量,使用循环或选择的简单语句。

//递归
constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}

if constexpr

C++17中引入了if constexpr条件编译来对循环进行优化,基于这样一种考虑,如果选择语句的结果在编译器就能知道,那么就在编译器找到选择的分支,抛弃别的子支。优点是优化运行时的效率,缺点是可能造成代码膨胀。

template<int N>
auto func(){
    if constexpr (N == 1)return 1;elsereturn -1;
}//调用
func<1>();
//上面的函数会变成
/*auto func(){return 1; } */

这里要注意的是if constexpr的条件必须是常量表达式,而且据我的观察,就连判断表达式,比如==两侧的对象必须都是常量表达式。这样使用的范围其实很窄,只有模板的非类型模板参数或者函数内部可能出现constexpr,而无法将参数用于条件。

  相关解决方案