declare begin end 中if怎么写_C++干货系列——C++17中的constexpr理论与实践

贴一句母校的“校训”——你能做的,岂止如此

在一段时间以前,我曾经写过编译期常量和constexpr。这篇文章的来由,一是因为C++17中对于编译期常量确实做了不少更新,值得一说;另外就是之前的文章并没有对如何运用编译期常量解决实际问题作出实例。

那么在这篇文章的上半部分,我们的目光会放在C++17中和constepxr有关的新特性;在文章的下半部分,我们将综合这四篇文章所涉及到的知识点,尝试编译期解决FizzBuzz问题,我会着重于介绍在处理编译期问题的过程和思路,而不是简单的把示例代码罗列出来。如果你对编译期常量和constexpr还一无所知,我建议你先看看之前的三篇文章(第一篇,第二篇和第三篇),不会很长。希望文章对你有所帮助。

C++17中的constexpr

这一部分会涉及到constexpr类型的lambda 表达式,constexpr if判断语句以及在C++标准库中的有关改进。

constexpr lambda表达式

在C++17后,lambda表达式就已经可以被声明为constexpr了。也就是说,他们可以被用在任何constexpr的上下文当中。同样的,对一个lambda而言,只要被捕获的变量是字面量类型(lieteral type),那么整个lambda也将表现为字面量类型

 //显式声明为constexpr类型
 template <typename T>
 constexpr auto addTo(T i) {
   return [i](auto j) {return i + j;};
 }
 ​
 constexpr auto add5 = addTo(5);
 ​
 template <unsigned N>
 class SomeClass{};
 ​
 int foo() {
   //在编译期常量中使用
   SomeClass<add5<22>> someClass27;
 }

当一个闭包在constexpr环境下被使用时,当它满足了constexpr的条件,无论它有没有被显式地声明为constexpr,它仍然是constexpr的。

 //这里没有显式声明为constexpr,但依然可以表现为constexpr
 auto answer = [](int n)
 {
   return 32 + n;
 };
 ​
 //在一个constexpr环境中被使用
 constexpr int response = answer(10);

当一个lambda表达式被显式或隐式地声明为constexpr,它可以被转换成一个constexpr的函数指针:

 auto Increment = [](int n)
 {
   return n + 1;
 };
 ​
 constexpr int(*int)(int) = Increment;

constexpr if

constexpr if让以前理应被写在一起,却在C++17前都没法被写在一起的情况得到了改善。例如在许多tag dispatch, enable_if和其他各种奇奇怪怪的标签被使用的场景中,if constexpr都可以大显身手。传统的if-else语句是在执行期进行条件判断与选择的,因而在泛型编程中,无法使用if-else语句进行条件判断,比如下面的代码就无法通过编译:

 template <class Head, class... Tail>
 void print(Head const& head, Tail const&... tail) {
   std::cout << head;
   if (sizeof...(tail) > 0) {     //Error
     std::cout << ", ";
     print(tail...);
   }
 }

在C++17标准以前,这些函数会只能被拆分写成一个范型版本的和一个特殊版本,在特殊版本中只有Head会被传进去做参数,范型版本中还有可变参数Tail被传进来。而constexpr让这两种情况合二为一,做出了编译时的语句判断。

 template <class Head, class... Tail>
 void print(Head const& head, Tail const&... tail) {
   std::cout << head;
   if constexpr(sizeof...(tail) > 0) {
     std::cout << ", ";
     print(tail...);
   }
 }

再比如,考虑一个将数值转化成字符串的函数,在C++17之前,我们需要大量的std::enable_if来判断参数类型,如下例:

template <typename T>
std::enable_if_t<std::is_integral<T>::value, std::string>
 to_string(T t){
   return std::to_string(t);
 }
 ​
template <typename T>
std::enable_if_t<!std::is_integral<T>::value, std::string>
 to_string(T t){
   return t;
 }

在C++17中,constexpr if可以实现相同的功能,不仅缩短代码量,还提高了可读性:

template <typename T>
auto to_string(T t) {
   if constexpr(std::is_integral<T>::value) {
     return std::to_string(t);
   } else {
     return t;
   }
 }

在C++17中,我们已经可以在编译期对传统的条件语句作出相应的判断了,相应的,编译器就可以忽略那些完全没有被进入的语句。其实即使没有C++17,如果你的if语句的条件是一个编译期常量,你的编译器和优化器也会做出相应的优化,来优化掉那些没有被进入的条件语句。

需要注意的是,在老的标准中,即使使用了if,另一个分支也仍然会被编译,但在C++17中,如果使用if constexpr来代替if,编译器甚至会把编译无效条件这个过程都省略掉了。当然了,另一条分支的语句仍然是要符合C++语法的,因为解释器至少要搞清楚if逻辑到底是在哪里结束的。

请看下边这个例子:

 template <typename T>
 auto someFunc(T t) {
   if constexpr(std::is_same_v<T, X>) {
     return t.some_func_only_for_x();
   } else {
     std::cout << t << std::endl;
     return;
   }
 }
 ​
 void callerFunc() {
   X x;
   auto res = someFunc(x);
   someFunc(25);
 }

在上边的例子中,函数some_func_only_for_x只有类X才有,所以如果使用老式的if语句,对于类似于someFunc(23)的调用会导致一个编译错误。除此之外,你会发现随着编译器进入不同条件语句,someFunc的返回值类型也是在发生变化的。对于传入X类型的参数,返回值类型是int,而对于其他类型则是void

实际上,上边的写法很像是编译器把两个分支分开,并创建了两个完全独立的函数:

 auto someFunc(X x) {
   return x.some_func_only_for_x();
 }
 ​
 template<typename T>
 auto someFunc(T t) {
   std::cout << t << std::endl;
 }

当然,如果这两个函数的功能毫无联系,我们确实也应该把他们分开写(除非X的那个函数一个什么诡异的打印功能),并且明确出不同的返回类型。当这两个函数只有名字很像,其实际功能不同时,就不要再把它们像上边一样写在一起了。

constexpr对STL标准库做出的改进

以前在标准库中,有许多类型和函数都缺乏了constexpr的特性,这些问题在C++17中都相应做了改进。最著名的就是std::array以及用于范围获取的std::begin()std::end()了。

也就是说,只要std::array包含的类型是字面量类型,std::array本身也将成为一个字面量类型,它的绝大多数操作也能在编译期就直接被处理。而std::begin()std::end()等则依赖于容器本身:既然std::vector不是一个字面量类型,std::begin(vec)也就不是constexpr类型的;但是std::begin(arr)对于C类型的数组以及std::array而言却是constexpr的。

使用constexpr在编译期解决FizzBuzz问题

FizzBuzz问题简介

这个问题是一个以前面试的时候非常常见的问题:请你写出一个程序,输出从1到N。但是对于每一个能被3整除的数字输出"fizz",能被5整除的数字输出"buzz",既能被3也能被5整除的数字输出"fizzbuzz"。

相信如果在run-time的情况下,你可以很轻松地写出如下程序,注意下边的程序中,我没有考虑任何可以优化的地方,你可以认为这只是一个草稿,只是给出一个示例,毕竟这不是我们今天讨论的重点。

 std::string nFizzBuzz(unsigned N) {
   std::string str;
   if(N % 3 == 0) {
     str += "fizz";
   }
   if(N % 5 == 5) {
     str += "buzz";
   }
   if(str.empty()) {
     str = std::to_string(N);
   }
   return str;
 }
 ​
 std::string fizzBuzz(unsigned N) {
   if( N <= 0) {
     return "";
   }
   std::string str = nFizzBuzz(1);
   for (unsigned n = 2; n <= N; n++) {
     str += ", " + nFizzBuzz(n);
   }
   return str;
 }

那么当你输入7的时候,以上代码就会输出:

 1, 2, fizz, 4, buzz, fizz, 7

编译期的解法

有了C++17标准后,我们代码的整体结构可以不做大的变动,但还是有一些run-time的代码我们无法在编译期使用:比如在C++17中编译期堆上的内存分配是不被允许的,因此std::stringstd::to_string也就行不通——我们必须为std::stringstd::to_string寻找合适的替代品。

解决这个问题,最耿直的做法就是使用std::array<char, Size>。基于这个想法,我们就要重写一个to_array()函数,其作用和std::to_string()基本相同。在下面的代码中,我给std::array<char, Size>起了一个别名,用到了using关键字(C++11),让代码更可读一些。基于不太成熟的想法,我们可能会把to_array()的函数原型设计成如下形式:

 template<std::size_t Size>
 using chars = std::array<char, Size>;
 ​
 constexpr chars<Size> to_array(unsigned N) {
   /*
   ...
   */
 }

结果我们马上就遇到了第一个难题:Size的值在编译期是什么?这其实取决于N,所以N就不能再是一个普通的函数参数了。这里的逻辑还是比较简单的:因为constexpr有可能在runtime被调用,因此有些值在compile-time我们是无法获取的,所以N必须强制被设为一个编译期常量——没错,就是模板参数

 unsigned n;
 std::cin >> n;
 auto number = to_array(n);

我们在编译期是无法知道n的值的,也就自然而然无法知道Size的大小。通常来说,一个compile-time函数的有关变量(这里的模板参数Size还有函数的返回值类型chars<Size>)是不能依赖于一个run-time函数的参数的。

因此,原arraySize和返回值类型我们最好让编译器为我们做决定,这里我们使用auto作为返回类型。这个函数本身看起来其实比较简单:

 template <unsigned N>
 constexpr auto to_array() {
   constexpr char lastDigit = '0' + N % 10;
   if constexpr(N >= 10) {
     return conct(to_chars<N / 10>(), chars<1>{lastDigit});
   } else {
     return chars<1>{lastDigit};
   }
 }

到这里为止,问题解决一半了。还有一个明显的问题就是,我们仍然需要给array构建一个类似于std::string+=操作符,我们称之为组合操作(concatenation)。因为在array中我们无法使用+=——两个长度不一样的array属于不同类型,无法通过直接相加得到,所以我们必须得手动实现它。

组合操作的思想其实是很简单的:如果我有一个长度分别为5和6的array,那么我就创建一个长度为11的array,再做两次array的拷贝将短数组的值拷贝到长数组中,任务就完成了。不过不幸的是,std::copy并不是constexpr的,因此这个函数我们也要自己实现。

 //在编译期拷贝first和last之间的数据到to上
constexpr void copy(char const* first, char const* last, char* to) {
   while(first < last) {
     *to++ - *first++;
   }
 }
 //在编译期将两个array组合起来,并返回一个组合后的array
 template <std::size_t N1, std::size_t N2>
 constexpr auto conct(
   chars<N1> const& array1, 
   chars<N2> const& array2) {
   chars<N1 + N2> res{};
   copy(str1.begin(), str1.end(), res.begin());        //begin()和end()函数也是constexpr的了
   copy(str2.begin(), str2.end(), res.begin() + N1);
   return res;
 }

其实这里我没有对copy函数和conct函数进行更复杂的泛化,是因为我们没必要让我们的代码更general,这么写也能够减少潜在的bug。

回到FizzBuzz问题

现在我们手上用来处理FizzBuzz问题的工具基本上准备的差不多了。就像to_array一样,nFizzBuzz函数和fizzBuzz函数也会将模板参数作为输入

 template <unsigned N>
 constexpr auto nFizzBuzz() {
   constexpr chars<4> FIZZ{'f', 'i', 'z', 'z'};
   constexpr chars<4> BUZZ{'b', 'u', 'z', 'z'};
   
   if constexpr (N % 3 == 0 && N % 5 == 0) {
     return conct(FIZZ, BUZZ);
   } else if constexpr (N % 3 == 0) {    //注意else后的if也要写成if constexpr
     return FIZZ;
   } else if constexpr (N % 5 == 0) {
     return BUZZ;
   } else {
     return to_array<N>();
   }
 }
 ​
 template <unsigned N>
 constexpr auto fizzBuzz() {
   constexpr chars<2> seperateChar{',', ' '};    //用于不同输出之间的间隔
   static_assert(N > 0);
   if constexpr (N != 1) {
     return conct(fizzBuzz<N - 1>()),
     conct(seperateChar, nFizzBuzz<N>());
   } else {
     return nFizzBuzz<N>();
   }
 }

当然,在这个示例中,我们还有很多可以值得改进的地方,比如将递归改进为迭代并在编译期实现多个array的组合,不过这些估计会放在日后自己实现了,因为这篇文章已经够长了。

结语

至今为止,我们使用constexpr都不能像使用run-time的一些工具一样得心应手,这是一件很正常的事。但是我们可以一步一步走向终点,就像上边我们举的例子一样。掌握了constexpr的技巧,我们仍然可以在编译期做很多事,巧妙地提高我们运行时的性能。

希望大家关注我的专栏:

C++干货系列​www.zhihu.com
13b3d2119ebbfef0d48e221bae2d00bf.png

白白

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值