第十章 编译期if
通过语法if constexpr(...)
,编译器使用编译时表达式在编译时决定是否使用then部分或者else部分。如果任一部分被抛弃,那部分代码就不会生成。但是,这不意味着被抛弃的部分完全被忽略了。它将像未使用模板的代码一样进行检查。
比如:
#include <string>
template <typename T>
std::string asString(T x)
{
if constexpr(std::is_same_v<T, std::string>) {
return x; // statement invalid, if no conversion to string
}
else if constexpr(std::is_arithmetic_v<T>) {
return std::to_string(x); // statement invalid, if x is not numeric
}
else {
return std::string(x); // statement invalid, if no conversion to string
}
}
这里我们用到了编译器if的特性。它在编译时决定我们是只返回一个字符串,还是说要调std::to_string()
把数组转称字符串,又或者把传进来的参数转成std::string
。因为无效的调用会被抛弃,下面的代码都可以编译:
#include "ifcomptime.hpp"
#include <iostream>
int main()
{
std::cout << asString(42) << '\n';
std::cout << asString(std::string("hello")) << '\n';
std::cout << asString("hello") << '\n';
}
10.1 存在编译期if的动机
如果刚刚的例子中,我们用运行时if:
#include <string>
template <typename T>
std::string asString(T x)
{
if (std::is_same_v<T, std::string>) {
return x; // ERROR, if no conversion to string
}
else if (std::is_numeric_v<T>) {
return std::to_string(x); // ERROR, if x is not numeric
}
else {
return std::string(x); // ERROR, if no conversion to string
}
}
对应的调用代码肯定不能编译。这是因为函数模板有个规则,要么全编,要么完全不编。if的条件检查是一个运行时行为。即使编译期就知道条件一定是个false,then也必须编。所以,当传递一个std::string
或者字符串字面值的时候,编译就出错了,因为对于传入的参数调用std::string
是无效的。当传递数值时,编译也出错,因为第一个和第三个返回语句是无效的。
注意,仅当使用编译期if的时候,then或者else没有被用到的才会被丢弃:
- 当传
std::string
时,第一个if的else部分被丢弃 - 当传一个数值时,第一个if的then部分和最后的else被丢弃
- 当传递一个字符串字面值时(即类型
const char*
),then和第二个if被丢弃
注意,被丢弃的语句不是说被忽略了。只是说当依赖模板参数时,它不会被实例化。语法必须正确,不依赖模板参数的调用也必须正确。事实上,在第一个翻译阶段(definition time)执行时,编译器会检查语法正确与否,使用的所有名字是否都不依赖模板参数。所有static_assert
也必须正确,即使该分支不会被编译。比如
template<typename T>
void foo(T t)
{
if constexpr(std::is_integral_v<T>) {
if (t > 0) {
foo(t-1); // OK
}
}
else {
undeclared(t); // error if not declared and not discarded (i.e., T is not integral)
undeclared(); // error if not declared (even if discarded)
static_assert(false, "no integral"); // always asserts (even if discarded)
}
}
只要是符合标准的编译器,这个例子都不会被编译,原因有两个:
- 即使T是整数类型,这个
undeclared(); // error if not declared (even if discarded)
没有声明的调用也是错的,因为它依赖模板参数
- 这个静态断言
static_assert(false, "no integral");
总是失败,即使它所在的分支会被丢弃,原因还是因为它依赖一个模板参数。重复编译期条件的静态断言是可以的:
static_assert(!std::is_integral_v<T>, "no integral");
注意,一些编译器(比如Visual C++2013和1025)没有正确的实现两阶段模板翻译,它们把绝大多第一阶段(definition time)该做的事情推迟到第二阶段(instantiation time),所以无效的函数调用,甚至一些语法错误也可能通过编译。
10.2 使用编译期if
原则上,你可以使用编译器if做一些事情,看起来就像是运行时的if,只是条件是编译期的表达式。你也可以混用编译期间if和运行时if:
if constexpr (std::is_integral_v<std::remove_reference_t<T>>) {
if (val > 10) {
if constexpr (std::numeric_limits<char>::is_signed) {
...
}
else {
...
}
}
else {
...
}
}
else {
...
}
注意你不能在函数体外面使用if constexpr
。因此,你不能在用它代替条件预处理器。
10.2.1 编译期if注意事项
即使有时看起来可以使用编译期if,有一些不明显的结果会出现,本小节会一一称述。
编译期if影响返回类型
编译期if可能影响函数的返回类型。比如,下面的代码总是可以编译,但是返回类型可能是不同的:
auto foo()
{
if constexpr (sizeof(int) > 4) {
return 42;
}
else {
return 42u;
}
}
因为我们用了auto,返回类型取决于返回语句,返回语句又取决于int的大小:
- 如果size大于4,只有一个有效的返回语句返回42,所以返回类型是_int_
- 否则,只有一个有效的返回语句返回42u,所以返回类型是_unsigned int_
事情还可能更魔幻。比如下面的例子,如果我们跳过else部分,返回类型可能是int或者void:
auto foo() // return type might be int or void
{
if constexpr (sizeof(int) > 4) {
return 42;
}
}
如果这里的if运行时if那代码就不能编译,因为两个返回语句都会被编译器考虑,最后得出的结论是返回类型存在二义性。
即便then返回了,else也可能造成问题
对于运行时if语句,有一\种模式不适用于编译期if:如果then和else都有返回语句,而且能通过编译,你总是可以跳过运行时if的else部分。换句话说,下面这种代码:
if (...) {
return a;
}
else {
return b;
}
你总是可以改写为:
if (...) {
return a;
}
return b;
这种模式不适用于编译期if,因为第二种形式的返回值取决于两个返回语句,而不是一个,可能会造成歧义。比如,改一下上面的例子,代码可能能编,也可能不能编:
auto foo()
{
if constexpr (sizeof(int) > 4) {
return 42;
}
return 42u;
}
如果条件是true,那么编译器推导出两个不同的返回类型,编不了。否则,只有一个返回语句,不会造成问题,所以可以编译。
编译期if的短路运算
考虑下面的代码:
template<typename T>
constexpr auto foo(const T& val)
{
if constexpr (std::is_integral<T>::value) {
if constexpr (T{} < 10) {
return val * 2;
}
}
return val;
}
我们有两个编译期条件,来决定是否直接返回val,或者翻倍再返回。 下面代码都可以编译:
constexpr auto x1 = foo(42); //产生84
constexpr auto x2 = foo("hi"); //可以的,产生"hi"
在运行时if中的条件可以进行短路运算。你可能期望编译其if也有这种能力:
template<typename T>
constexpr auto bar(const T& val)
{
if constexpr (std::is_integral<T>::value && T{} < 10) {
return val * 2;
}
return val;
}
然而,编译期if的条件总是被实例化,需要作为整体来确定是否有效,所以传入一个不支持<10
判断的类型编不了:
constexpr auto x2 = bar("hi"); // compile-time ERROR
所以,编译期if是不会短路实例化的。如果编译期条件的有效性取决于更早期的编译期条件,你不得不嵌套一下,也就是说,你不得不这样写:
if constexpr (std::is_same_v<MyType, T>) {
if constexpr (T::i == 42) {
...
}
}
而不是:
if constexpr (std::is_same_v<MyType, T> && T::i == 42) {
...
}
10.2.2 其他编译期if的例子
返回完美转发
一个编译期if的应用是返回值的完美转发。因为decltype(auto)
不能被推导为void(因为void是不完全类型(incomplete type)),你必须这样写:
#include <functional> // for std::forward()
#include <type_traits> // for std::is_same<> and std::invoke_result<>
template<typename Callable, typename... Args>
decltype(auto) call(Callable op, Args&&... args)
{
if constexpr(std::is_void_v<std::invoke_result_t<Callable, Args...>>) {
// return type is void:
op(std::forward<Args>(args)...);
... // do something before we return
return;
}
else {
// return type is not void:
decltype(auto) ret{op(std::forward<Args>(args)...)};
... // do something (with ret) before we return
return ret;
}
}
编译期的tag派发
编译期if的一个传统应用是tag派发。在C++17之前,你必为你希望处理的类型提供完整的函数重载集合。现在有了编译期if,你可以把所有逻辑放到一个函数里面。举个例子,你可以不用像下面这样写一堆重载函数来实现std::advance()
算法:
template<typename Iterator, typename Distance>
void advance(Iterator& pos, Distance n) {
using cat = std::iterator_traits<Iterator>::iterator_category;
advanceImpl(pos, n, cat); // tag dispatch over iterator category
}
template<typename Iterator, typename Distance>
void advanceImpl(Iterator& pos, Distance n,
std::random_access_iterator_tag) {
pos += n;
}
template<typename Iterator, typename Distance>
void advanceImpl(Iterator& pos, Distance n,
std::bidirectional_iterator_tag) {
if (n >= 0) {
while (n--) {
++pos;
}
}
else {
while (n++) {
--pos;
}
}
}
template<typename Iterator, typename Distance>
void advanceImpl(Iterator& pos, Distance n, std::input_iterator_tag) {
while (n--) {
++pos;
}
}
而是将所有行为在一个函数里面实现:
template<typename Iterator, typename Distance>
void advance(Iterator& pos, Distance n) {
using cat = std::iterator_traits<Iterator>::iterator_category;
if constexpr (std::is_same_v<cat, std::random_access_iterator_tag>) {
pos += n;
}
else if constexpr (std::is_same_v<cat,
std::bidirectional_access_iterator_tag>) {
if (n >= 0) {
while (n--) {
++pos;
}
}
else {
while (n++) {
--pos;
}
}
}
else { // input_iterator_tag
while (n--) {
++pos;
}
}
}
在某种程度上,我们现在有一个编译期switch,虽然不同case是通过if constexpr
来表达。注意,这里有一个区别:
- 重载函数集合给你最佳匹配(best match)语意
- 编译期if给你第一匹配(fisrt match)语意
另一个tag派发的例子是第一章里面使用编译期if的get<>()
重载。
第三个例子是第十六章里面处理不同类型的std::variant<>()
访问器。
10.3 编译期if初始化
注意编译期if也可以用于新的带初始化的if语法中。比如,如果有个constexpr函数foo()
,你可以使用:
template<typename T>
void bar(const T x)
{
if constexpr (auto obj = foo(x); std::is_same_v<decltype(obj), T>) {
std::cout << "foo(x) yields same type\n";
...
}
else {
std::cout << "foo(x) yields different type\n";
...
}
}
你可以像上面一样让bar根据foo产生结果的值的类型是否与T相同,来产生不同的行为。
要根据foo(x)
返回的值本身来决定不同行为,你可以这样:
constexpr auto c = ...;
if constexpr (constexpr auto obj = foo(c); obj == 0) {
std::cout << "foo() == 0\n";
...
}
obj
必须声明为constexpr,因为要在条件中使用它的值。
10.4 在模板外面使用编译期if
if constexpr
可以被用于任何函数,不仅仅局限于模板。我们只需要编译期表达式产生的结果可以转换为bool值。然而,在那种情况下then和else的中的所有语句都必须是有效地,即便它们可能被抛弃。
比如,下面的代码总是不能编译,因为即便char是signed、else被抛弃,undeclared()
这个调用也必须有效才行:
#include <limits>
template<typename T>
void foo(T t);
int main()
{
if constexpr(std::numeric_limits<char>::is_signed) {
foo(42); // OK
}
else {
undeclared(42); // ALWAYS ERROR if not declared (even if discarded)
}
}
下面的代码也不能编译,因为其中一个静态断言总是会失败:
if constexpr(std::numeric_limits<char>::is_signed) {
static_assert(std::numeric_limits<char>::is_signed);
}
else {
static_assert(!std::numeric_limits<char>::is_signed);
}
在模板代码外面使用编译期if唯一的好处是那些被抛弃的语句(必须有效)不需要编到最后的二进制代码中,减小了可执行程序的体积。比如下面的程序:
#include <limits>
#include <string>
#include <array>
int main()
{
if (!std::numeric_limits<char>::is_signed) {
static std::array<std::string,1000> arr1;
...
}
else {
static std::array<std::string,1000> arr2;
...
}
}
arr1或者arr2是最终可执行程序的一部分,但不会都是。
10.5 后记
编译期if最初由Walter Bright,Herb Sutter和Andrei Alexandrescu在https://wg21.link/n3329中提出。Ville Voutilainen在https://wg21.link/n4461提出了static if语言特性。在https://wg21.link/p0128r0中Ville Voutilainen第一次提出了constexpr_if(这个feature名字的起源)。最后的公认措辞是由Jens Maurer在https://wg21.link/p0292r2中给出。