C++17之编译时期if

在使用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都是最终可执行文件的一部分,但不是两者都是。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值