分享

代码在编译期就能执行运算C++还有谁?

 山峰云绕 2023-02-19 发布于贵州

https://m.toutiao.com/is/BWyvGbJ/ 


什么是编译期编程?

编译期编程就是把程序的计算过程提前到编译期,能带来可观的性能提升

· 普通编程:代码在运行程序的时候执行

· 编译期编程:代码在编译程序的时候执行

C++常见的编译器编程是模板元编程。C++模板给C++提供了元编程的能力,但大部分用户对 C++ 模板的使用并不是很频繁,大致限于泛型编程,在一些系统级的代码,尤其是对通用性、性能要求极高的基础库(如 STL、Boost)几乎不可避免在大量地使用 C++ 模板以及模板元编程。

模版元编程完全不同于普通的运行期程序,因为模版元程序的执行完全是在编译期并且模版元程序操纵的数据不能是运行时变量,只能是编译期常量,不可修改

C++ 模板是图灵完备的,这使得 C++代码存在两个层次,其中,执行编译计算的代码称为静态代码(static code),执行运行期计算的代码称为动态代码(dynamic code),C++的静态代码由模板实现,编写C++的静态代码,就是进行C++的模板元编程。

模板元编程在编译过程中的位置请见下图:

图片来自网络

为什么要使用编译期编程?

C++ 模板可以做以下事情:编译期数值计算、类型计算、代码计算(如循环展开),其中数值计算实际意义不大,而类型计算和代码计算可以使得代码更加通用,更加易用,性能更好

只能在运行期间解析的虚函数调用是不允许使用内联的。这往往会造成性能问题,该问题我们必须解决。因为函数调用的动态绑定是继承的结果,所以消除动态绑定的一种方法是用基于模板的设计来替代继承。

模板把解析的步骤从运行期间提前到编译期间,从这个意义上说,模板提高了性能。而对于我们所关心的编译时间,适当增加也是可以接受的。

C++11开始,针对模板元编程的一些缺点,增加了constexpr关键词,优化了元编程代码的编写方式,也解决了代码膨胀问题。

备注:“代码膨胀问题”是指代码编译时的模板代码会根据实际调用传入参数展开大量实例代码,后面我们可以通过cppinsight工具看出来这种问题。

示例场景

下面我们从计算阶乘示例看看这两种编译器编程的区别。

阶乘的 数学定义 :

! = *( - 1) !

0! = 1

模板元编程

模版元编程概念介绍

模版元程序由元数据元函数组成,元数据就是元编程可以操作的数据,即C++编译器在编译期可以操作的数据。

元数据不是运行期变量,只能是编译期常量,不能修改,常见的元数据有enum枚举常量、静态常量、基本类型和自定义类型等

元函数是模板元编程中用于操作处理元数据的“构件”,可以在编译期被“调用”,因为它的功能和形式 和 运行时的函数类似,而被称为元函数,它是元编程中最重要的构件。

元函数实际上表现为C++的一个类、模板类或模板函数,元函数只处理元数据,元数据是编译期的常量静态类型,不能是普通变量。

模板元编程产生的源程序是在编译期执行的程序,因此它首先要遵循C++和模板的语法,但是它操作的对象不是运行时普通的变量,因此不能使用运行时的C++关键字(如if、else、for),可用的语法元素相当有限,最常用的是:​​​​​

enum、static const //用来定义编译期的整数常量;typedef/using    //用于定义元数据;[类型别名]T/Args...      //声明元数据类型;【模版参数:类型形参,非类型形参】Template     //主要用于定义元函数;【模版类,特化,偏特化】::        //域运算符,用于解析类型作用域获取计算结果(元数据)。【获取元数据,元类型】

模版元编程示例代码

下面我们分别使用enum和static const来定义编译期的整数常量进行编码测试。

//枚举版本#include <iostream>using namespace std;//模板----阶乘template <uint64_t N>struct Fact{    enum{ Value = N * Fact<N - 1>::Value };};//特化template <>struct Fact<1>{    enum{ Value = 1 };};int main(){    auto value = Fact<26>::Value;    cout <<value<<endl;    return 0;}

代码展开状态:

可以看出来,代码编译时的模板代码,会根据实际调用传入参数展开大量实例代码,即“代码膨胀问题”。

通过汇编代码可以看出数值在编译时已经计算出来了。

注意:在编译器编译enum类型的数值计算模板元编程代码时,有的编译器会提示枚举类型不一致无法进行运算,可以换成使用static const类型(推荐用此方式)。

//static const 版本#include <iostream>using namespace std;//模板----阶乘template <uint64_t N>struct Fact{ static const int Value = N * Fact<N - 1>::Value;};//特化template <>struct Fact<1>{ static const int Value = 1;};int main(){ auto value = Fact<26>::Value; cout <<value<<endl; return 0;}

模板元编程的问题

  • 代码比较难写、难阅读、更难调试;
  • 会有代码膨胀问题;
  • 编译会变慢(因为代码膨胀和数值计算等原因);
  • 可能耗费大量内存;
  • 元编程计算的数值过大容易让编译器崩溃,甚至让系统失去响应;
  • 对于C++来说,由于各编译器的差异,大量依赖模板元编程(特别是最新形式的)的代码可能会有移植性的问题

constexpr介绍

c++11后,可以通过constexpr修饰递归函数实现在编译期实现数据的计算。

constexpr修饰的函数在编译期中会优化成inline类型

constexpr的演变

1、constexpr语法从C++11开始支持,constexpr除了可以作为常量表达式修饰常量数据外,还支持编译期编程。

2、C++14支持变量模板:

template<class T> constexpr T pi = T(3.14156);printf('%d\n',pi<int>); // 3printf('%lf\n',pi<double>); // 3.141560

3、constexpr的限制

C++11中constexpr函数可以使用递归,在C++14中可以使用局部变量和循环:

constexpr int factorial(int n) { // C++14 和 C++11均可return n <= 1 ? 1 : (n * factorial(n - 1));}

在C++14中可以这样做:

constexpr int factorial(int n) { // C++11中不可,C++14中可以int ret = 0;for (int i = 0; i < n; ++i) {ret += i;}return ret;}

C++17开始支持constexpr lambda表达式:

C++17前lambda表达式只能在运行时使用,C++17引入了constexpr lambda表达式,可以用于在编译期进行计算。

constexpr auto func = [](auto i){return i*i;};static_assert(func(2) == 4);

注意:函数体不能包含汇编语句、goto语句、label、try块、静态变量、线程局部存储、没有初始化的普通变量,不能动态分配内存,不能有new delete等,不能虚函数。

4、C++11中constexpr函数必须把所有东西都放在一个单独的return语句中,而C++14中constexpr则无此限制:

constexpr int func(bool flag) { // C++11中不可,C++14中可以if (flag) return 1;else return 0;}

constexpr编程示例代码

下面我们分别使用constexpr来定义编译期函数返回值进行编码测试(测试程序使用c++20进行编译)。

//constexpr 版本#include <iostream>using namespace std;constexpr auto Fact(uint64_t N){ if (1 == N) { return N;} return N * Fact(N - 1);};int main(){ auto value = Fact(26); cout <<value<<endl; return 0;}

使用在线工具查看代码展开效果和汇编代码:

从上图可以看出来,constexpr修饰的元编程在编译时不像模板元编程那样展开大量代码。

通过汇编代码可以看出数值在编译时已经计算出来了。

constexpr使用习惯建议

在编写模板元编程代码时,模板的“<>”中的模板参数相当于函数调用的输入参数,参数必须写成常量数值。那么C++新特性中是否支持constexpr的编译期运行函数传入普通参数呢?

我们来测试一下把Fact(6)的参数从常量改成局部变量或者外面函数传递过来的参数。

我们发现,编译是没有问题的,只是数值计算不再于编译期计算,而是在运行期计算了,就达不到元编程的效果了。

为了防止在编码时“因参数没写成常量导致元编程失效”的问题,建议可以通过constexpr修饰函数返回值赋值的变量,就是告诉编译器这行代码调用的函数要在编译期进行计算,这时调用函数的参数只能传常量如果传递的类型不是常量,编译器会报错提醒。

举例如下,

constexpr编程注意点

  • 使用constexpr修饰函数的话,尽量修饰非递归函数,这样代码运行能更高效。
  • 函数返回值赋值的变量去除constexpr修饰符后,就是在运行期计算了,此时函数参数也允许传入变量了(有修饰符时只能传常量)。
  • 支持-std=c++14及以上编译器,才可以使用constexpr修饰函数。
  • 在编译期运行的代码里,就不要有运行时执行的函数,比如printf等操作。

总结

· 编译期编程提供了在编译期进行推理、计算的能力

· 模板元编程在 C++ 发展的早期提供编译期编程的能力

· 现代 C++ 提供了更多编译期编程的特性,更加易用


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多