我所知道的 constexpr
分类:技术
在知乎上看到《constexpr 究竟有什么用?》的问题,正巧前段时间趁着了解 constexpr if 的时候刚做过相关资料的搜集,拼凑了一下搞了一个回答。篇幅所限,完整版放在这里,主要是追加一些例子并补完一些周边内容 😄
constexpr
关键词是 C++11 引入的;C++14 中放宽了对 constexpr 函数的语法要求;而 C++17 则复用了该关键字,引入了 constexpr if。
constexpr
主要为 C++03 引入了以下变动:
- 拓展了「常量表达式」的概念
- 提供了「强制要求」表达式在编译时求值(compile-time evaluation)的方法
- 提供了编译时的
if
条件判断
拓展常量表达式的概念
之所以要拓展常量表达式的概念,是因为 C++03 中的一些尴尬,这从原本的标准库中就能看出来。
比如我们都知道 INT_MAX
是 C 语言的遗产,C++ 则更希望大家使用 std::numeric_limits<int>::max()
来拿 int
型的上限。然而不幸的是,后者是个函数调用而不是整型常量,使用起来可能需要花更多心思在性能或者别的东西上,没有前者那么自由。
int foo[std::numeric_limits<char>::max()]; // error: 不是常量表达式
又比如标准文件流,它的构造函数可以带上这样的第二个参数:
std::fstream foo("foo.txt", std::ios::in | std::ios::out);
这个参数是 openmode
类型的,是由实现具体定义的一种 Bitmask 类型。出于类型安全的考虑,通常用枚举值实现而不是整型。但是这样一来就有个问题,同样是写 std::ios::in | std::ios::out
,如果用整型的话可以作为常量表达式使用,而为了类型安全考虑换用枚举实现(尤其是重载 operator|
)后,就再也不可能是常量表达式了。
inline openmode operator|(openmode lhs, openmode rhs)
{
return openmode(int_type(lhs) | int_type(rhs));
}
明明是这样简单的函数,对它的调用却不是常量表达式,就更别提编译时求值了。这就让委员会陷入了必须在「类型安全」和「效率」里二选一的尴尬境地。
标准库里会遇到这样的问题,大家日常使用也会遇到。加之标准委员会很想借此机会把原本标准中对于「常量表达式」(尤其是整型常量表达式)复杂的定义重构简化,引入 constexpr
来标记一些简单的函数,让对它们的调用能够作为常量表达式存在就很合情合理了。
而 constexpr 成员函数也可以类推,比独立的 constexpr 函数多一些对于类本身的限制便是了。
至于 constexpr 函数「到底应该多简单」,其实并没有必要刻意去记那些限制。如果你标记了 constexpr
的函数不够简单,编译器会提醒你哪里有问题的。
强制要求表达式编译时求值
说到这里,「constexpr 函数」并不能和「编译时求值」划等号,它只表达了「这个函数具备这样的能力」。
如果需要强制要求表达式在编译时求值,那么我们需要在变量定义前添加 constexpr
,这样,用来初始化这个变量的表达式就「必须」是常量表达式,否则会报错。
#include <fstream>
// 该函数不是 constexpr 函数
int identity(std::ios::openmode mode)
{
return mode;
}
constexpr auto out = std::ios::out; // 此处为 constexpr
int main()
{
constexpr int modeFail = std::ios::in | identity(out); // 出错
int modePass = std::ios::in | identity(out); // 正常
}
constexpr 函数只有同时满足
- 所有参数都是常量表达式
- 返回的结果被用于常量表达式(比如用于初始化 constexpr 数据)
才会触发编译时求值。如果只有参数是常量表达式而结果不是,那么是否触发编译时求值取决于具体实现。
constexpr if
C++17 的 constexpr if 严格意义上不是 constexpr
而是 if
的一部分。
constexpr if 的主要用途是简化模板代码(这也意味着除非你是「库作者」或者「模版狂魔」,很少会用到)。很多原本需要绕弯借助类型 Tag 或者 SFINAE 来实现,需要拆成 N 个函数的功能,可以借助 constexpr if 写到一个函数里。
以下面的 get_value
函数为例。这个函数正常情况下会把参数原样返回,但如果传入指针,则返回被指针指向的内容。
如果用简单的类型 Tag 来实现,需要拆成三个模板:
template <typename T>
auto get_value(T t, std::true_type) {
return *t;
}
template <typename T>
auto get_value(T t, std::false_type) {
return t;
}
template <typename T>
auto get_value(T t) {
return get_value(t, std::is_pointer<T>{});
}
如果用 SFINAE 来实现,需要拆成两个模板:
template <typename T, std::enable_if_t<std::is_pointer_v<T>, int> = 0>
auto get_value(T t) {
return *t;
}
template <typename T, typename std::enable_if_t<!std::is_pointer_v<T>, int> = 0>
auto get_value(T t) {
return t;
}
而如果用 constexpr if 来实现,可以简化为一个:
template <typename T>
auto get_value(T t)
{
if constexpr (std::is_pointer_v<T>) {
return *t;
} else {
return t;
}
}
也有人把 constexpr if 拿来代替当作 #if
用,相信你可以想象得出具体的用法。但……我觉得那是邪道,所以不写了 😛
constexpr
和 const
最初接触到 C++11 时,constexpr
和 const
的关系让我很是害怕,因为排列组合似乎很多。不过这回终于下了决心,整理了下,没有初见时想象的那么复杂,总共似乎也有两个情况。
在修饰数据(变量)时,constexpr
是隐含 const
语义的。同时,constexpr
只和变量本身有关,遇到类似 const int *p
和 int *const p
的破事时,建议还是写明:
constexpr int const foo = 42;
constexpr int foo = 42; // 和上面等价
constexpr int const *pb = &bar; // 此处 &bar 必须是常量表达式
在涉及 constexpr 成员函数时,成员函数可能会有 const
修饰的情况。C++11 中,constexpr 成员函数是隐含 const
的;C++14 以来则没有这个限制。这里似乎可以借用 Python 的哲学「Better explicit than implicit」来表达一下,总共就几个字母的事情,就别偷懒了,你没法保证别人和你一样清楚这些「不那么直观」的东西。
以上就是所有内容了,感觉还是有些流水账的样子。
顺便小小吐槽一下「模板」,macOS 自带的拼音输入法只能通过 mú bǎn 打出来,所以我老是打成「模版」……
"拓宽「常量表达式」的范围" 那部分顺带介绍一下 literal type? (于是加了一吨内容
哈哈哈~那就真变成吨的手册了 😂