现代C++中的constexpr,内敛变量
constexpr
简单认识constexpr
编译期编程很烧脑,为此有时候一个简单的计算也需要写大量代码,C++为此做了很多改进,让编译期编程简单了很多。当然对于一些没有改进的东西仍需要模板元编程,因此模板元编程仍然是很重要的一部分。
考虑下面代码是否可以运行?
1 |
|
两条报错都提到了constexpr,constexpr在C++11引入,在C++14大幅改进,简单来说就是在编译期确定完全确定的常数。我们分两类讨论这个关键字:
- constexpr 变量
- constexpr 函数
一个constexpr变量是一个编译时完全确定的常数,一个constexpr函数至少对于某一组实参可以在编译期间确定。
注意一个 constexpr 函数不保证在所有情况下都会产生一个编译期常数(因而也是可以作为普通函数来使用的)。编译器也没法通用地检查这点。编译器唯一强制的是:
- constexpr 变量必须立即初始化,初始化只能使用字面量或常量表达式(constexpr)
现在可以运行了:
1 |
|
注意仅仅修改n为expr你仍然会得到编译报错,因为一个constexpr的变量需要 字面量或者constexpr来初始化他,因此sqr函数也需要改成constexpr的。
constexpr和编译期计算
还记得模板元编程阶乘吗?有了constexpr这件事情可以做的更加简单直接:
1 |
|
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
15struct 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 |
|
你会发现链接时就报错,因为找不到magic::number
,类还没有实例化,所以肯定找不到。这是因为 ODR-use
的类静态常量也需要有一个定义,在没有内联变量之前需要在某一个源代码文件(非头文件,头文件如果被多次include会导致重复定义报错)中这样:
1 |
|
必须正正好好一个,多了少了都不行,所以叫 one definition rule。内联函数,现在又有了内联变量的新概念,以及模板,则不受这条规则限制。
有了内敛变量后这个操作可以做的优雅一些:
magic
里的static const
改成 static constexpr
: 因为 类的静态 constexpr 成员变量默认就是内联的
magic
里的static const
改成static inline const
: const 常量和类外面的 constexpr 变量不默认内联,需要手工加 inline
关键字才会变成内联。
以上两种操作都是可行的,在汇编层面也是一摸一样的。
constexpr变量仍是const
有时候我们还是需要 constexpr和const两者一起出现的:
比如:
1 |
|
符合我们直觉可能是下面的做法,但是下面的会报错。这是因为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 |
|
C++17: if constexpr
to do
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!