在使用if constexpr(…)语法时,编译器在编译时期使用编译时期表达式决定是否使用if语句或then部分或else部分(如果有的话)。如果其中一个分支条件成立,则另一部分(如果有的话)被丢弃,这样也就不会生成被丢弃部分的代码。但是,这并不意味着被丢弃的部分完全被忽略,它就像未使用模板的代码一样检查语法等。
例 1:
#include <iostream>
#include <string>
template <typename T>
std::string asString(T x)
{
if constexpr (std::is_same_v<T, std::string>) {//第一个判断语句
std::cout << "satement 1 output: ";
return x; // statement invalid, if no conversion to string
}
else if constexpr (std::is_arithmetic_v<T>) { 第二个判断语句
std::cout << "satement 2 output: ";
return std::to_string(x); // statement invalid, if x is not numeric
}
else { 第三个判断语句
std::cout << "satement 3 output: ";
return std::string(x); // statement invalid, if no conversion to string
}
}
int main()
{
std::cout << asString(42) << '\n';
std::cout << asString(std::string("hello")) << '\n';
std::cout << asString("hello") << '\n';
std::cout << asString("x2");
}
这里使用编译时期if这个特性来决定在编译时是否只返回传递的字符串,或者传递的参数是整数或浮点值则调用std::to_string()做为返回值,或者尝试将传递的参数转换为std::string。由于无效的调用会被丢弃,上面的代码将被编译(如果使用常规运行时,则不会出现这种情况)。
结果如下:
使用compile-time if语法后,其声明如下:
- 传递一个std::string值,第二个判断语句和第三个判断语句丢弃;
- 传递一个数值,第一个判断语句和第三个判断语句丢弃;
- 传递一个字符串字面值,第一个判断语句和第二个判断语句丢弃。
所以,在编译时期的if语句只要配到了合法的语句,其他的语句都被丢弃了。
1. compile-time if的动机
如果对于上面介绍的代码不使用compile-if,按照运行时的方式写
#include <iostream>
#include <string>
template <typename T>
std::string asString(T x)
{
if (std::is_same_v<T, std::string>) {
return x; // statement invalid, if no conversion to string
}
else if (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
}
}
int main()
{
std::cout << asString(42) << '\n';
std::cout << asString(std::string("hello")) << '\n';
std::cout << asString("hello") << '\n';
std::cout << asString("x2");
}
这段代码是无法编译的。这是因为函数模板要么不被编译[没有调用]要么做为一个整体编译,不会出现只编译部分的情况,因为if条件的检查是一个运行时特性。上面代码在编译时,当传递std::string或字符串字面值时,因为传递的参数无法使用std::to_string()调用,编译会失败;当传递数值时,编译也会失败,因为第一个和第三个判断返回语句无效。
注意,丢弃代码语句的语发检查不会被忽略,其结果是,当依赖于模板参数时,它不会被实例化[丢弃了说明没有被调用]。不依赖于模板参数的调用语法必须正确。所有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函数在else部分虽然被丢弃,但是undeclared函数不依赖于模板参数,没有声明这个函数编译错误。
- static_assert所在那部分的语句即便被丢弃,其语法必须正确,因为static_assert不依赖于模板参数。
2. 使用compile-time 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。因此,不能使用它来替换条件预处理器指令。
2.1编译时注意事项
- 编译时if影响返回值
编译时if可能会影响函数的返回类型。例如,下面的代码总是编译,但是返回类型可能不同:
#include <iostream>
auto foo()
{
if constexpr (sizeof(int) > 4)
{
return 42;
}
else
{
return 42u;
}
}
int main(void)
{
auto ret = foo();
return 0;
}
这里,因为我们使用auto,函数的返回类型取决于返回语句,返回语句取决于int的大小:
a. 如果int大小大于4,则只有一个返回42的有效返回语句,因此返回类型为int。
b. 否则,只有一个返回语句返回42u,因此返回类型变为无符号整型。
注意,如果这里使用的是运行时if,那么这段代码永远不会编译,因为这两个返回语句都会被考虑进去,因此返回类型的推断是不明确的。
这样,if constexpr函数的返回类型可能会有更大的差异。例如,如果我们没有else部分,返回类型可能是int或void:
auto foo() // return type might be int or void
{
if constexpr (sizeof(int) > 4)
{
return 42;
}
}
- if...else...返回类型的其他影响
对于运行时if语句,有一种模式不适用于编译时if语句:如果在then和else部分都编译带有返回语句的代码,则始终可以跳过运行时if语句中的else。也就是说,
if (...)
{
return a;
}
else
{
return b;
}
可以替换为:
if (...)
{
return a;
}
return b;
这种模式不适用于编译时if,因为在第二种形式中,返回类型依赖于两个返回语句,而不是一个返回语句,这可能会造成不同。例如,修改上面的例子会导致代码可能会编译(constexpr if为false),也可能不会编译:
auto foo()
{
if constexpr (sizeof(int) > 4)
{
return 42;
}
return 42u;
}
如果条件为真(int的大小大于4),编译器推断出两种不同的返回类型,这是无效的。否则,我们只有一个重要的返回语句,这样代码才能编译。
再举个具体的例子:
#include <iostream>
#include <type_traits>
template<class T> auto minus(T a, T b)
{
if constexpr (std::is_same<T, double>::value)
{
if (std::abs(a - b) < 0.0001)
{
return 0.;
}
else
{
return a - b;
}
}
else
{
return static_cast<int>(a - b);
}
}
int main()
{
std::cout << minus(5.6, 5.11) << std::endl;
std::cout << minus(5.60002, 5.600011) << std::endl;
std::cout << minus(6, 5) << std::endl;
}
以上是一个带精度限制的减法函数,当参数类型为double且计算结果小于0.0001的时候,我们就可以认为计算结果为0。当参数类型为整型时,则不用对精度做任何限制。上面的代码编译运行没有任何问题,因为编译器根据不同的类型选择不同的分支进行编译。但是如果修改一下上面的代码,结果可能就很难预料了:
#include <iostream>
#include <type_traits>
template<class T> auto minus(T a, T b)
{
if constexpr (std::is_same<T, double>::value)
{
if (std::abs(a - b) < 0.0001)
{
return 0.;
}
else
{
return a - b;
}
}
return static_cast<int>(a - b);
}
int main()
{
std::cout << minus(5.6, 5.11) << std::endl;
std::cout << minus(5.60002, 5.600011) << std::endl;
std::cout << minus(6, 5) << std::endl;
}
当实参为整型时一切正常,编译器会忽略if的代码块,直接编译return static_cast<int>(a − b),这样返回类型只有int一种。但是当实参类型为double的时候,情况发生了变化。if的代码块会被正常地编译,代码块内部的返回结果类型为double,而代码块外部的return static_cast<int>(a − b)
同样会照常编译,这次的返回类型为int。编译器遇到了两个不同的返回类型,只能报错。
- 短路条件编译时
考虑以下代码:
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;
}
这里,我们有两个编译时条件来决定是返回传递的原值,还是返回乘以2的值。
这编译为两个:
constexpr auto x1 = foo(42); // yields 84
constexpr auto x2 = foo("hi"); // OK, yields ”hi”
运行时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中做为整体语句实例化代码)。
如果编译时条件的有效性依赖于早期的编译时条件,则必须像在foo()中那样嵌套它们。另一个例子,你必须写:
if constexpr (std::is_same_v<MyType, T>)
{
if constexpr (T::i == 42)
{
...
}
}
代替:
if constexpr (std::is_same_v<MyType, T> && T::i == 42)
{
...
}
举个例子:
#include <iostream>
#include <string>
#include <type_traits>
template<class T> auto any2i(T t)
{
if constexpr (std::is_same<T, std::string>::value && T::npos ==-1)
{
return atoi(t.c_str());
}
else
{
return t;
}
}
int main()
{
std::cout << any2i(std::string("6")) << std::endl;
//std::cout << any2i(6) << std::endl;//error: type 'int' cannot be used prior to '::' because it has no members
return 0;
}
上面的代码很好理解,函数模板any2i的实参如果是一个std::string,那么它肯定满足std::is_same<T,std::string>::value && T::npos == −1的条件,所以编译器会编译if分支的代码。
当函数实参为int时,std::is_same<T, std::string>::value和T::npos == −1都会被编译,由于int::npos显然是一个非法的表达式,因此会造成编译失败。这里正确的写法是通过嵌套if constexpr来替换上面的操作:
#include <iostream>
#include <string>
#include <type_traits>
template<class T> auto any2i(T t)
{
if constexpr (std::is_same<T, std::string>::value)
{
if constexpr(T::npos == -1)
{
return atoi(t.c_str());
}
}
else
{
return t;
}
}
int main()
{
std::cout << any2i(std::string("6")) << std::endl;
std::cout << any2i(6) << std::endl;//OK
return 0;
}
2.2 其他编译时if的例子
- 泛型值的完美返回
编译时if的一个应用程序是返回值的完美转发,在返回值之前必须对它们进行处理。因为decltype(auto)不能对void进行推导
(因为它是一个不完整的类型),你必须这样写:
#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...>>)
{
op(std::forward<Args>(args)...);
... // do something before we return
return; // return type is void:
}
else
{
decltype(auto) ret{op(std::forward<Args>(args)...)};
... // do something (with ret) before we return
return ret; // return type is not void:
}
}
- 编译时if的标记调度
编译时if的一个典型应用是标记调度。在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;
}
}
}
在某种程度上,我们现在有一个编译时切换,不同的情况必须由if constexpr子句来表示。不过,请注意一个可能很重要的区别:
- 这组重载函数为您提供了最佳匹配语义。
- 带有编译时if的实现提供了第一个匹配语义。
3.3 编译时if初始化
编译时if还可以使用if的新形式进行初始化。例如,如果有一个constexpr函数foo()可以接受传递的类型,则可以使用此代码提供关于foo(x)是否生成与x相同的类型的不同行为。:
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";
...
}
}
要决定foo(x)返回的值,可以这样写:
constexpr auto c = ...;
if constexpr (constexpr auto obj = foo(c); obj == 0)
{
std::cout << "foo() == 0\n";
...
}
注意,obj必须声明为constexpr才能在条件中使用它的值。
3.4 模板外使用编译时if
如果constexpr可以用在任何函数中,而不仅仅是模板中。我们只需要一个编译时表达式,生成一些可转换为bool的东西。然而,在这种情况下,then和else部分中的所有语句都必须是有效的,即使被丢弃。
例如,下面的代码总是无法编译,因为undeclare()的调用必须是有效的,即使字符被签名,其他部分被丢弃:
#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;
...
}
}
ar1或arr2都是最终可执行文件的一部分,但不是两者都是。