• constexpr if
  • constexpr lambda
  • fold expression
  • void_t
  • structureed binding
  • std::apply std::invoke
  • string_view
  • parallel STL
  • inline variable

来自boost库,比如variant,any,optional,filesystem等;
语法糖:if init、deduction guide、guaranteed copy Elision、template、nested namespace、single param static_assert等。

fold exception

主要针对C++11的可变模板参数展开问题。

fold exception语义

fold expression有四种语义:

  1. unary right fold (pack op…)
  2. unary left fold (…op pack)
  3. binary right fold (pack op … op init)
  4. binary right fold (init pack op … op)

其中pack为变参,op为操作符。

  1. 一元左折叠(unary left fold
  2. ( ... op pack )
  3. 一元左折叠 (... op E) 展开之后变为 ((E1 op E2) op ...) op En
  4. 一元右折叠(unary right fold
  5. ( pack op ... )
  6. 一元右折叠 (E op ...) 展开之后变为 E1 op (... op (En-1 op En))
  7. 二元左折叠(binary left fold
  8. ( init op ... op pack )
  9. 二元左折叠 (I op ... op E) 展开之后变为 (((I op E1) op E2) op ...) op En
  10. 二元右折叠(binary right fold
  11. ( pack op ... op init )
  12. 二元右折叠 (E op ... op I) 展开之后变为 E1 op (... op (EN1 op (EN op I)))

unary right fold

从表达式右边开始fold,看它是left fold还是right fold我们可以根据参数包…所在的位置来判断,当参数包…在操作符右边的时候就是right fold,在左边的时候就是left fold。

  1. template<typename... Args>
  2. auto add_val(Args&&... args)
  3. {
  4. return (args + ...);
  5. }
  6. auto t = add_val(1,2,3,4); //10

unary left fold

对于+这种满足交换律的操作符来说,left fold和right fold是一样的,比如上面的例子你也可以写成left fold。

  1. template<typename... Args>
  2. auto add_val(Args&&... args)
  3. {
  4. return (... + args);
  5. }
  6. auto t = add_val(1,2,3,4); //10

对于不满足交换律的操作符来说就要注意了,比如减法,下面的right fold和left fold的结果就不一样。

  1. template<typename... Args>
  2. auto sub_val_right(Args&&... args)
  3. {
  4. return (args - ...);
  5. }
  6. template<typename... Args>
  7. auto sub_val_left(Args&&... args)
  8. {
  9. return (... - args);
  10. }
  11. auto t = sub_val_right(2,3,4); //(2-(3-4)) = 3
  12. auto t1 = sub_val_left(2,3,4); //((2-3)-4) = -5

binary right fold

二元fold的语义和一元fold的语义是相同的,参数包…在左即二元左折叠,参数包…在右即右折叠。下面看一个二元右折叠的例子。

  1. template<typename... Args>
  2. auto sub_one_left(Args&&... args)
  3. {
  4. return (1 - ... - args);
  5. }
  6. auto t = sub_one_right234);//(2-(3-(4-1))) = 2

binary left fold

  1. template<typename... Args>
  2. auto sub_one_right(Args&&... args)
  3. {
  4. return (args - ... - 1);
  5. }
  6. auto t = sub_one_left234);// (((1-2)-3)-4) = -8

comma fold

在C++17之前,我们经常使用逗号表达式结合列表初始化的方式对参数包进行展开,比如像下面这个例子:

  1. template<typename T>
  2. void print_arg(T t)
  3. {
  4. std::cout << t << std::endl;
  5. }
  6. template<typename... Args>
  7. void print2(Args... args)
  8. {
  9. int a[] = { (print_arg(args), 0)... };
  10. //或者
  11. //std::initializer_list<int>{(print_arg(args), 0)...};
  12. }

这种写法比较繁琐,用fold expression就会变得很简单了。

  1. //unary right fold
  2. template<typename... Args>
  3. void print3(Args... args)
  4. {
  5. (print_arg(args), ...);
  6. }
  7. //unary left fold
  8. template<typename... Args>
  9. void print3(Args... args)
  10. {
  11. (..., print_arg(args));
  12. }

unary right fold和unary left fold,对于comma来说两种写法是一样的,参数都是从左至右传入print_arg函数。当然,我们也可以通过binary fold实现:

  1. template<typename ...Args>
  2. void printer(Args&&... args)
  3. {
  4. (std::cout << ... << args) << '\n';
  5. }

注意,下面的写法是不合法的,根据binary fold的语法,参数包…必须在操作符中间。

  1. template<typename ...Args>
  2. void printer(Args&&... args)
  3. {
  4. (std::cout << args << ...) << '\n';
  5. }

constexpr if

constexpr标记一个表达式或一个函数的返回结果是编译期常量,它保证函数会在编译期执行。相比模版来说,实现编译期循环或递归,C++17中的constexpr if会让代码变得更简洁易懂。比如实现一个编译期整数加法:

  1. template<int N>
  2. constexpr int sum()
  3. {
  4. return N;
  5. }
  6. template <int N, int N2, int... Ns>
  7. constexpr int sum()
  8. {
  9. return N + sum<N2, Ns...>();
  10. }

C++17之前你可能需要像上面这样写,但是现在你可以写更简洁的代码了。

  1. template <int N, int... Ns>
  2. constexpr auto sum17()
  3. {
  4. if constexpr (sizeof...(Ns) == 0)
  5. return N;
  6. else
  7. return N + sum17<Ns...>();
  8. }

当然,你也可以用C++17的fold expression:

  1. template<typename ...Args>
  2. constexpr int sum(Args... args) {
  3. return (0 + ... + args);
  4. }

constexpr还可以用来消除enable_if了,对于讨厌写一长串enable_if的人来说会非常开心。比如我需要根据类型来选择函数的时候:

  1. template<typename T>
  2. std::enable_if_t<std::is_integral<T>::value, std::string> to_str(T t)
  3. {
  4. return std::to_string(t);
  5. }
  6. template<typename T>
  7. std::enable_if_t<!std::is_integral<T>::value, std::string> to_str(T t)
  8. {
  9. return t;
  10. }

经常不得不分开几个函数来写,还需要写长长的enable_if,比较繁琐,通过if constexpr可以消除enable_if了。

  1. template<typename T>
  2. auto to_str17(T t)
  3. {
  4. if constexpr(std::is_integral<T>::value)
  5. return std::to_string(t);
  6. else
  7. return t;
  8. }

constexpr if让C++的模版具备if-else if-else功能了,是不是很酷,C++程序员的好日子来了。
不过需要注意的是下面这种写法是有问题的。

  1. template<typename T>
  2. auto to_str17(T t)
  3. {
  4. if constexpr(std::is_integral<T>::value)
  5. return std::to_string(t);
  6. return t;
  7. }

这个代码把else去掉了,当输入如果是非数字类型时代码可以编译过,以为if constexpr在模版实例化的时候会丢弃不满足条件的部分,因此函数体中的前两行代码将失效,只有最后一句有效。当输入的为数字的时候就会产生编译错误了,因为if constexpr满足条件了,这时候就会有两个return了,就会导致编译错误。
constexpr if还可以用来替换#ifdef宏,看下面的例子

  1. enum class OS { Linux, Mac, Windows };
  2. //Translate the macros to C++ at a single point in the application
  3. #ifdef __linux__
  4. constexpr OS the_os = OS::Linux;
  5. #elif __APPLE__
  6. constexpr OS the_os = OS::Mac;
  7. #elif __WIN32
  8. constexpr OS the_os = OS::Windows;
  9. #endif
  10. void do_something() {
  11. //do something general
  12. if constexpr (the_os == OS::Linux) {
  13. //do something Linuxy
  14. }
  15. else if constexpr (the_os == OS::Mac) {
  16. //do something Appley
  17. }
  18. else if constexpr (the_os == OS::Windows) {
  19. //do something Windowsy
  20. }
  21. //do something general
  22. }

代码变得更清爽了,再也不需要像以前一样写#ifdef那样难看的代码块了。

constexpr lambda

constexpr lambda其实很简单,它的意思就是可以在constexpr 函数中用lambda表达式了,这在C++17之前是不允许的。这样使用constexpr函数和普通函数没多大区别了,使用起来非常舒服。下面是constexpr lambda的例子:

  1. template <typename I>
  2. constexpr auto func(I i) {
  3. //use a lambda in constexpr context
  4. return [i](auto j){ return i + j; };
  5. }

string_view

string_view的基本用法

C++17中的string_view是一个char数据的视图或者说引用,它并不拥有该数据,是为了避免拷贝,因此使用string_view可以用来做性能优化。你应该用string_view来代替const char和const string了。string_view的方法和string类似,用法很简单:

  1. const char* data = "test";
  2. std::string_view str1(data, 4);
  3. std::cout<<str1.length()<<'\n'; //4
  4. if(data==str1)
  5. std::cout<<"ok"<<'\n';
  6. const std::string str2 = "test";
  7. std::string_view str3(str2, str2.size());

构造string_view的时候用char*和长度来构造,这个长度可以自由确定,它表示string_view希望引用的字符串的长度。因为它只是引用其他字符串,所以它不会分配内存,不会像string那样容易产生临时变量。我们通过一个测试程序来看看string_view如何来帮我们优化性能的。

  1. using namespace std::literals;
  2. constexpr auto s = "it is a test"sv;
  3. auto str = "it is a test"s;
  4. constexpr int LEN = 1000000;
  5. boost::timer t;
  6. for (int i = 0; i < LEN; ++i) {
  7. constexpr auto s1 = s.substr(3);
  8. }
  9. std::cout<<t.elapsed()<<'\n';
  10. t.restart();
  11. for (int i = 0; i < LEN; ++i) {
  12. auto s2 = str.substr(3);
  13. }
  14. std::cout<<t.elapsed()<<'\n';
  15. //output
  16. 0.004197
  17. 0.231505

我们可以通过字面量””sv来初始化string_view。string_view的substr和string的substr相比,快了50多倍,根本原因是它不会分配内存。

string_view的生命周期

由于string_vew并不拥有锁引用的字符串,所以它也不会去关注被引用字符串的生命周期,用户在使用的时候需要注意,不要将一个临时变量给一个string_view,那样会导致string_view引用的内容也失效。

  1. std::string_view str_v;
  2. {
  3. std::string temp = "test";
  4. str_v = {temp};
  5. }

这样的代码是有问题的,因为出了作用域之后,string_view引用的内容已经失效了。