现代C++中的constexpr,内敛变量

constexpr

简单认识constexpr

编译期编程很烧脑,为此有时候一个简单的计算也需要写大量代码,C++为此做了很多改进,让编译期编程简单了很多。当然对于一些没有改进的东西仍需要模板元编程,因此模板元编程仍然是很重要的一部分。

考虑下面代码是否可以运行?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sqr(int n)
{
return n*n;
}
int main()
{
std::array<int, sqr(3)> a;//error: call to non-'constexpr' function 'int sqr(int)'|

const int n = sqr(3);
std::array<int, n> b;//error: the value of 'n' is not usable in a constant expression|


return 0;
}

两条报错都提到了constexpr,constexpr在C++11引入,在C++14大幅改进,简单来说就是在编译期确定完全确定的常数。我们分两类讨论这个关键字:

  • constexpr 变量
  • constexpr 函数

一个constexpr变量是一个编译时完全确定的常数,一个constexpr函数至少对于某一组实参可以在编译期间确定。

注意一个 constexpr 函数不保证在所有情况下都会产生一个编译期常数(因而也是可以作为普通函数来使用的)。编译器也没法通用地检查这点。编译器唯一强制的是:

  • constexpr 变量必须立即初始化,初始化只能使用字面量或常量表达式(constexpr)

现在可以运行了:

1
2
3
4
5
6
7
8
9
10
11
constexpr int sqr(int n)
{
return n*n;
}
int main()
{
constexpr int n = sqr(3);
std::array<int, n> c;//ok

return 0;
}

注意仅仅修改n为expr你仍然会得到编译报错,因为一个constexpr的变量需要 字面量或者constexpr来初始化他,因此sqr函数也需要改成constexpr的。

constexpr和编译期计算

还记得模板元编程阶乘吗?有了constexpr这件事情可以做的更加简单直接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
constexpr int fac(int n)
{
if(n == 0)
{
return 1;
}
else
{
return n*fac(n-1);
}
}
int main()
{
constexpr int n = fac(10);
cout<<n<<endl;

return 0;
}

image-20220721104849492

const/constexpr,C++17的内敛变量inline

早期时,const设计的理念是运行时常量,但随着C++发展后面带上了constexpr用法,也表示编译期常数

因此在有了 constexpr 之后——我们应该使用 constexpr 在这些用法中替换 const了。从编译器的角度,为了向后兼容性,const 和 constexpr 在很多情况下还是等价的。

当然也有一些小区别:比如是否内联的问题。

在C++17出现了新的概念叫做内敛变量

https://blog.csdn.net/jiemashizhen/article/details/125531625 建议先看看这篇文章大致了解下内敛变量

看了上面的文章我们可以总结下在C++17之前:

  • static变量是不建议定义在头文件中(注意这是定义,不是声明,声明是可以在头文件中的,写在class里只能叫做声明,类实例化后里面的东西才被定义),因为这大概率会导致头文件被多次include会导致重复定义报错,现在有了内敛变量是可以在头文件里定义而不会导致重复定义错误了。

  • 非const的static变量在C++17前必须在类外声明,这很不方便,现在有了内敛变量可以直接:inline static int number = 42

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct magic
    {
    //error: ISO C++ forbids in-class initialization of non-const static member 'magic::number'|
    static int number = 42;
    };

    struct magic
    {
    inline static int number = 12;//ok,after c++ 17
    };

    struct magic
    {
    constexpr static int number = 12;//ok,after c++ 17
    };
  • 解决ODR-use的问题,如下:

C++17后允许在头文件中定义内联变量,然后像内联函数一样,只要所有的定义都相同,那变量的定义出现多次也没有关系。对于类的静态数据成员,const 缺省inline是不内联的,而 constexpr 缺省inline就是内联的。这种区别在你用 &去取一个 const int值的地址、或将其传到一个形参类型为 const int&的函数去的时候(这在 C++ 文档里的行话叫 ODR-use), 如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct magic
{
static const int number = 42;
};

int main()
{
cout<<magic::number<<endl;//ok
int t = magic::number + 1;//ok,这两种用法只是需要number的值,所以number没被定义也是可以的。

vector<int>v;
// 调用 push_back(const T&)
v.push_back(magic::number);//undefined reference to `magic::number'| 在链接时就会报错了,说找不到 magic::number,这里需要一个左值,所以定义肯定找不到。
cout<<v[0]<<endl;

return 0;
}

你会发现链接时就报错,因为找不到magic::number,类还没有实例化,所以肯定找不到。这是因为 ODR-use 的类静态常量也需要有一个定义,在没有内联变量之前需要在某一个源代码文件(非头文件,头文件如果被多次include会导致重复定义报错)中这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* .h */
struct magic
{
static const int number = 42;
};

/* .cpp */
const int magic::number;

int main()
{
vector<int>v;
// 调用 push_back(const T&)
v.push_back(magic::number);//undefined reference to `magic::number'| 在链接时就会报错了,说找不到 magic::number
cout<<v[0]<<endl;

return 0;
}

必须正正好好一个,多了少了都不行,所以叫 one definition rule。内联函数,现在又有了内联变量的新概念,以及模板,则不受这条规则限制。

有了内敛变量后这个操作可以做的优雅一些:

magic里的static const 改成 static constexpr : 因为 类的静态 constexpr 成员变量默认就是内联的

magic里的static const 改成static inline const: const 常量和类外面的 constexpr 变量不默认内联,需要手工加 inline关键字才会变成内联。

以上两种操作都是可行的,在汇编层面也是一摸一样的。

image-20220723154544163

image-20220723154624043

constexpr变量仍是const

有时候我们还是需要 constexpr和const两者一起出现的:

比如:

1
2
3
constexpr int a = 42;
constexpr const int& b = a;//ok
constexpr int& b = a;//error: binding reference of type ‘int&’ to ‘const int’ discards qualifiers

符合我们直觉可能是下面的做法,但是下面的会报错。这是因为constexpr修饰的是b是一个编译器常量。而int&会被编译器解读为将一个普通引用绑定到const int上。

因此我们可以发现constepr好像并没有const那样位置组合规则,他就是单纯的修饰一个常量, constexpr 不需要像 const 一样有复杂的组合,因此永远是写在类型前面的。

constexpr构造函数/字面类型

constexpr在每个C++版本都会出现一些新的东西:

  • 最早,constexpr 函数里连循环都不能有,但在 C++14 放开了。

  • 目前,constexpr 函数仍不能有 try … catch 语句和 asm 声明,但到 C++20 会放开。

  • constexpr 函数里不能使用 goto 语句。

一个有意思的情况是一个类的构造函数。如果一个类的构造函数里面只包含常量表达式、满足对 constexpr 函数的限制的话(这也意味着,里面不可以有任何动态内存分配),并且类的析构函数是平凡的,那这个类就可以被称为是一个字面类型。换一个角度想,对constexpr 函数——包括字面类型构造函数——的要求是,得让编译器能在编译期进行计算,而不会产生任何“副作用”,比如内存分配、输入、输出等等。

为了全面支持编译期计算,C++14 开始,很多标准类的构造函数和成员函数已经被标为constexpr,以便在编译期使用。当然,大部分的容器类,因为用到了动态内存分配,不能成为字面类型。下面这些不使用动态内存分配的字面类型则可以在常量表达式中使用:

  • array
  • initializer_list
  • pair
  • tuple
  • string_view
  • optional
  • variant
  • bitset
  • complex
  • chrono::duration
  • chrono::time_point
  • shared_ptr(仅限默认构造和空指针构造)
  • unique_ptr(仅限默认构造和空指针构造)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <array>
#include <iostream>
#include <memory>
#include <string_view>
using namespace std;
int main()
{
constexpr string_view sv{"hi"};
constexpr pair pr{sv[0], sv[1]};
constexpr array a{pr.first, pr.second};
constexpr int n1 = a[0];
constexpr int n2 = a[1];
//编译器可以在编译期即决定 n1 和 n2 的数值;
cout << n1 << ' ' << n2 << '\n';
}

image-20220723163901451

C++17: if constexpr

to do


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!