Modern C++ ——constexpr的各种用法

Modern C++ ——constexpr的用法

Reference

《现代C++语言核心特性解析》、《深入理解C++11》

为什么引入constexpr

  1. const可以定义常量,但也可以用来定义只读变量。const变量的值不一定是在编译期可以确定的,比如当通过函数返回值进行初始化时。

  2. C++标准委员会决定在C++11标准中定义一个新的关键字constexpr,它能够有效地定义常量表达式,并且达到类型安全、可移植、方便库和嵌入式系统开发的目的。

constexpr值

  1. constexpr值即常量表达式值,该值必须编译期能够确定。

  2. 常量表达式值必须被常量表达式初始化。

constexpr函数 (C++11)

(1)什么是constexpr函数?

constexpr不仅能用来定义常量表达式值,还能定义一个常量表达式函数,即constexpr函数,常量表达式函数的返回值可以在编译阶段就计算出来。

(2)C++11约束规则

1.函数必须返回一个值,所以它的返回值类型不能是void。
2.函数体必须只有一条语句:return expr,其中expr必须也是一个常量表达式。如果函数有形参,则将形参替换到expr中后,expr仍然必须是一个常量表达式。
3.函数使用之前必须有定义。
4.函数必须用constexpr声明。

(3)特殊情况

  1. 用递归完成循环,实现函数体只有单语句。
//编译失败
constexpr int sum(int x)
{
  int result = 0;
  while (x > 0)
  {
       result += x--;
  }
  return result;
}

//可改为
constexpr int sum(int x)
{
  return x > 0 ? x + sum(x - 1) : 0;
}
  1. if else双分支可以用条件表达式替代
//编译失败
constexpr int abs2(int x)
{
  if (x > 0) {
       return x;
  } else {
       return -x;
  }
}
//可改为
constexpr int sum(int x)
{
  return x > 0 ? x  : -x;
}
  1. 当带形参的常量表达式函数接受了一个非常量实参时,常量表达式函数可能会退化为普通函数。

constexpr构造函数

(1)用途

constexpr可以声明基础类型从而获得常量表达式值,除此之外constexpr还能够声明用户自定义类型。

(2)相关规则:

1.构造函数必须用constexpr声明。
2.构造函数初始化列表中必须是常量表达式。
3.构造函数的函数体必须为空(这一点基于构造函数没有返回值,所以不存在return expr)。

(3)注意

最后需要强调的是,使用constexpr声明自定义类型的变量,必须确保这个自定义类型的析构函数是平凡的(Trivial destructor),否则也是无法通过编译的。平凡析构函数必须满足下面3个条件。
1.自定义类型中不能有用户自定义的析构函数。2.析构函数不能是虚函数。3.基类和成员的析构函数必须都是平凡的。

支持浮点数

在constexpr说明符被引入之前,C++程序员经常使用enum hack来促使编译器在编译阶段计算常量表达式的值。但是因为enum只能操作整型,所以一直无法完成对于浮点类型的编译期计算。constexpr说明符则不同,它支持声明浮点类型的常量表达式值,而且标准还规定其精度必须至少和运行时的精度相同

constexpr用于模板函数

常量表达式是可以用于模板函数的。不过由于模板中类型的不确定性,所以模板函数是否会被实例化为一个能够满足编译时常量性的版本通常也是未知的。针对这种情况,C++11标准规定,当声明为常量表达式的模板函数后,而某个该模板函数的实例化结果不满足常量表达式的需求的话,constexpr会被自动忽略。

constexpr模板元编程

这种基于编译时期的运算的编程方式在C++中并不是第一次出现。早在C++模板刚出现的时候,就出现了基于模板的编译时期运算的编程方式,这种编程通常被称为模板元编程(template meta-programming)。

    #include <iostream>
    using namespace std;
    template <long num>
    struct Fibonacci{
        static const long val = Fibonacci<num -1>::val + Fibonacci<num -2>::val;
    };
    template <> struct Fibonacci<2> { static const long val = 1; };
    template <> struct Fibonacci<1> { static const long val = 1; };
    template <> struct Fibonacci<0> { static const long val = 0; };
    int main() {
        int fib[] = {
            Fibonacci<11>::val, Fibonacci<12>::val,
            Fibonacci<13>::val, Fibonacci<14>::val,
            Fibonacci<15>::val, Fibonacci<16>::val,
        };
        for (int i : fib) cout << i << endl;
    }
    // 编译选项:g++ -std=c++11 6-1-8.cpp 

我们定义了一个非类型参数的模板Fibonacci。该模板类定义了一个静态变量val,而val的定义方式是递归的。因此模板将会递归地进行推导。此外,我们还通过偏特化定义了模板推导的边界条件,即斐波那契的初始值。那么模板在推导到边界条件的时候就会终止推导。通过这样的方法,我们同样可以在编译时进行值计算,从而生成数组的值。

89
144
233
377
610
987

C++14对常量表达式函数的增强

1.函数体允许声明变量,除了没有初始化、static和thread_local变量。
2.函数允许出现if和switch语句,不能使用go语句。
3.函数允许所有的循环语句,包括for、while、do-while。
4.函数可以修改生命周期和常量表达式相同的对象。
5.函数的返回值可以声明为void。
6.constexpr声明的成员函数不再具有const属性。

#include <iostream>

class X {
public:
  constexpr X() : x1(5) {}
  constexpr X(int i) : x1(0)
  {
      if (i > 0) {  
          x1 = 5;
      }
      else {
          x1 = 8;
      }
  }
  constexpr void set(int i)  // 函数的返回值可以声明为constexpr void。 可以在常量表达式函数中被调用
  {
      x1 = i;
  }
  constexpr int get() const
  {
      return x1;
  }
private:
  int x1;
};

constexpr X make_x()
{
  X x;
  x.set(42);  
  return x;
}

int main()
{
  constexpr X x1(-1);
  constexpr X x2 = make_x();
  constexpr int a1 = x1.get();
  constexpr int a2 = x2.get();
  std::cout << a1 << std::endl;
  std::cout << a2 << std::endl;
}

C++17 constexpr lambda表达式

待续。

posted on 2023-02-10 14:18  七昂的技术之旅  阅读(477)  评论(0编辑  收藏  举报

导航