转自:http://blog.jobbole.com/112106/
引用
C++17标准在2017上半年已经讨论确定,正在形成ISO标准文档,今年晚些时候会正式发布。本文将介绍最新标准中值得开发者关注的新特新和基本用法。
总的来说C++17相比C++11的新特性来说新特性不算多,做了一些小幅改进。C++17增加了数十项新特性,值得关注的特性大概有下面这些:
- constexpr if
- constexpr lambda
- fold expression
- void_t
- structured binding
- std::apply, std::invoke
- string_view
- parallel STL
- inline variable
剩下的有一些来自于boost库,比如variant,any、optional和filesystem等特性,string_view其实在boost里也有。还有一些是语法糖,比如if init、deduction guide、guaranteed copy Elision、template、nested namespace、single param static_assert等特性。我接下来会介绍C++17主要的一些特性,介绍它们的基本用法和作用,让读者对C++17的新特性有一个基本的了解。
fold expression
C++11增加了一个新特性可变模版参数(variadic template),它可以接受任意个模版参数在参数包中,参数包是三个点…,它不能直接展开,需要通过一些特殊的方法才能展开,导致在使用的时候有点难度。现在C++17解决了这个问题,让参数包的展开变得容易了,Fold expression就是方便展开参数包的。
fold expression的语义
fold expression有4种语义:
- unary right fold (pack op …)
- unary left fold (… op pack)
- binary right fold (pack op … op init)
- binary left fold (init op … op pack)
其中pack代表变参,比如args,op代表操作符,fold expression支持32种操作符:
引用
1
|
+
-
*
/
%
^
&
|
=
>
+=
-=
*=
/=
%=
^=
&=
|=
>=
==
!=
=
&&
||
,
.
*
->
*
|
unary right fold的含义
fold (E op …) 意味着 E1 op (… op (EN-1 op EN)).
顾名思义,从右边开始fold,看它是left fold还是right fold我们可以根据参数包…所在的位置来判断,当参数包…在操作符右边的时候就是right fold,在左边的时候就是left fold。我们来看一个具体的例子:
1
2
3
4
5
6
|
template
<
typename
.
.
.
Args
>
auto
add_val
(
Args
&&
.
.
.
args
)
{
return
(
args
+
.
.
.
)
;
}
auto
t
=
add_val
(
1
,
2
,
3
,
4
)
;
//10
|
right fold的过程是这样的:(1+(2+(3+4))),从右边开始fold。
unary left fold的含义
fold (… op E) 意味着 ((E1 op E2) op …) op EN。
对于+这种满足交换律的操作符来说left fold和right fold是一样的,比如上面的例子你也可以写成left fold。
1
2
3
4
5
6
|
template
<
typename
.
.
.
Args
>
auto
add_val
(
Args
&&
.
.
.
args
)
{
return
(
.
.
.
+
args
)
;
}
auto
t
=
add_val
(
1
,
2
,
3
,
4
)
;
//10
|
对于不满足交换律的操作符来说就要注意了,比如减法。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
template
<
typename
.
.
.
Args
>
auto
sub_val_right
(
Args
&&
.
.
.
args
)
{
return
(
args
-
.
.
.
)
;
}
template
<
typename
.
.
.
Args
>
auto
sub_val_left
(
Args
&&
.
.
.
args
)
{
return
(
.
.
.
-
args
)
;
}
auto
t
=
sub_val_right
(
2
,
3
,
4
)
;
//(2-(3-4)) = 3
auto
t1
=
sub_val_left
(
2
,
3
,
4
)
;
//((2-3)-4) = -5
|
这次right fold和left fold的结果就不一样。
binary fold的含义
Binary right fold (E op … op I) 意味着 E1 op (… op (EN-1 op (EN op I)))。
Binary left fold (I op … op E) 意味着 (((I op E1) op E2) op …) op E2。
其中E代表变参,比如args,op代表操作符,I代表一个初始变量。
二元fold的语义和一元fold的语义是相同的,看一个二元操作符的例子:
1
2
3
4
5
6
7
8
9
10
11
12
|
template
<
typename
.
.
.
Args
>
auto
sub_one_left
(
Args
&&
.
.
.
args
)
{
return
(
1
-
.
.
.
-
args
)
;
}
template
<
typename
.
.
.
Args
>
auto
sub_one_right
(
Args
&&
.
.
.
args
)
{
return
(
args
-
.
.
.
-
1
)
;
}
auto
t
=
sub_one
_left(
2,
3,
4
)
;
// (((1-2)-3)-4) = -8
auto
t1
=
sub_one
_right(
2,
3,
4
)
;
//(2-(3-(4-1))) = 2
|
相信通过这个例子大家应该对C++17的fold expression有了基本的了解。
comma fold
在C++17之前,我们经常使用逗号表达式和std::initializer_list来将变参一个个传入一个函数。比如像下面这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
|
template
<
typename
T
>
void
print_arg
(
T
t
)
{
std
::
cout
<<
t
<<
std
::
endl
;
}
template
<
typename
.
.
.
Args
>
void
print2
(
Args
.
.
.
args
)
{
//int a[] = { (printarg(args), 0)... };
std
::
initializer_list
<
int
>
{
(
print_arg
(
args
)
,
0
)
.
.
.
}
;
}
|
这种写法比较繁琐,用fold expression就会变得很简单了。
1
2
3
4
5
|
template
<
typename
.
.
.
Args
>
void
print3
(
Args
.
.
.
args
)
{
(
print_arg
(
args
)
,
.
.
.
)
;
}
|
这是right fold,你也可以写成left fold,对于comma来说两种写法是一样的,参数都是从左至右传入print_arg函数。
1
2
3
4
5
|
template
<
typename
.
.
.
Args
>
void
print3
(
Args
.
.
.
args
)
{
(
.
.
.
,
print_arg
(
args
)
)
;
}
|
你也可以通过binary fold这样写:
1
2
3
4
|
template
<
typename
.
.
.
Args
>
void
printer
(
Args
&&
.
.
.
args
)
{
(
std
::
cout
<<
.
.
.
<<
args
)
<<
'\n'
;
}
|
也许你会觉得能写成这样:
1
2
3
4
|
template
<
typename
.
.
.
Args
>
void
printer
(
Args
&&
.
.
.
args
)
{
(
std
::
cout
<<
args
<<
.
.
.
)
<<
'\n'
;
}
|
但这样写是不合法的,根据binary fold的语法,参数包…必须在操作符中间,因此上面的这种写法不符合语法要求。
借助comma fold我们可以简化代码,假如我们希望实现tuple的for_each算法,像这样:
1
|
for_each
(
std
::
make_tuple
(
2.5
,
10
,
'a'
)
,
[
]
(
auto
e
)
{
std
::
cout
<<
e
<<
'\n'
;
}
)
;
|
这个for_each将会遍历tuple的元素并打印出来。在C++17之前我们如果要实现这个算法的话,需要借助逗号表达式和std::initializer_list来实现,类似于这样:
1
2
3
4
|
template
<
typename
.
.
.
Args
,
typename
Func
,
std
::
size_t
.
.
.
Idx
>
void
for_each
(
const
std
::
tuple
&
t
,
Func
&&
f
,
std
::
index_sequence
<
Idx
.
.
.
>
)
{
(
void
)
std
::
initializer_list
<
int
>
{
(
f
(
std
::
get
<
Idx
>
(
t
)
)
,
void
(
)
,
0
)
.
.
.
}
;
}
|
这样写比较繁琐不直观,现在借助fold expression我们可以简化代码了。
1
2
3
4
|
template
<
typename
.
.
.
Args
,
typename
Func
,
std
::
size_t
.
.
.
Idx
>
void
for_each
(
const
std
::
tuple
<
Args
.
.
.
>
&
t
,
Func
&&
f
,
std
::
index_sequence
<
Idx
.
.
.
>
)
{
(
f
(
std
::
get
<
Idx
>
(
t
)
)
,
.
.
.
)
;
}
|
借助coma fold我们可以写很简洁的代码了。
constexpr if
constexpr标记一个表达式或一个函数的返回结果是编译期常量,它保证函数会在编译期执行。相比模版来说,实现编译期循环或递归,C++17中的constexpr if会让代码变得更简洁易懂。比如实现一个编译期整数加法:
1
2
3
4
5
6
7
8
9
10
11
|
template
<
int
N
>
constexpr
int
sum
(
)
{
return
N
;
}
template
<
int
N
,
int
N2
,
int
.
.
.
Ns
>
constexpr
int
sum
(
)
{
return
N
+
sum
<
N2
,
Ns
.
.
.
>
(
)
;
}
|
C++17之前你可能需要像上面这样写,但是现在你可以写更简洁的代码了。
1
2
3
4
5
6
7
8
|
template
<
int
N
,
int
.
.
.
Ns
>
constexpr
auto
sum17
(
)
{
if
constexpr
(
sizeof
.
.
.
(
Ns
)
==
0
)
return
N
;
else
return
N
+
sum17
<
Ns
.
.
.
>
(
)
;
}
|
当然,你也可以用C++17的fold expression:
1
2
3
4
|
template
<
typename
.
.
.
Args
>
constexpr
int
sum
(
Args
.
.
.
args
)
{
return
(
0
+
.
.
.
+
args
)
;
}
|
constexpr还可以用来消除enable_if了,对于讨厌写一长串enable_if的人来说会非常开心。比如我需要根据类型来选择函数的时候:
1
2
3
4
5
6
7
8
9
10
11
|
template
<
typename
T
>
std
::
enable_if_t
<
std
::
is_integral
<
T
>
::
value
,
std
::
string
>
to_str
(
T
t
)
{
return
std
::
to_string
(
t
)
;
}
template
<
typename
T
>
std
::
enable_if_t
<
!
std
::
is_integral
<
T
>
::
value
,
std
::
string
>
to_str
(
T
t
)
{
return
t
;
}
|
经常不得不分开几个函数来写,还需要写长长的enable_if,比较繁琐,通过if constexpr可以消除enable_if了。
1
2
3
4
5
6
7
8
|
template
<
typename
T
>
auto
to_str17
(
T
t
)
{
if
constexpr
(
std
::
is_integral
<
T
>
::
value
)
return
std
::
to_string
(
t
)
;
else
return
t
;
}
|
constexpr if让C++的模版具备if-else if-else功能了,是不是很酷,C++程序员的好日子来了。
不过需要注意的是下面这种写法是有问题的。
1
2
3
4
5
6
7
8
|
template
<
typename
T
>
auto
to_str17
(
T
t
)
{
if
constexpr
(
std
::
is_integral
<
T
>
::
value
)
return
std
::
to_string
(
t
)
;
return
t
;
}
|
这个代码把else去掉了,当输入如果是非数字类型时代码可以编译过,以为if constexpr在模版实例化的时候会丢弃不满足条件的部分,因此函数体中的前两行代码将失效,只有最后一句有效。当输入的为数字的时候就会产生编译错误了,因为if constexpr满足条件了,这时候就会有两个return了,就会导致编译错误。
constexpr if还可以用来替换#ifdef宏,看下面的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
enum
class
OS
{
Linux
,
Mac
,
Windows
}
;
//Translate the macros to C++ at a single point in the application
#ifdef __linux__
constexpr
OS
the_os
=
OS
::
Linux
;
#elif __APPLE__
constexpr
OS
the_os
=
OS
::
Mac
;
#elif __WIN32
constexpr
OS
the_os
=
OS
::
Windows
;
#endif
void
do_something
(
)
{
//do something general
if
constexpr
(
the_os
==
OS
::
Linux
)
{
//do something Linuxy
}
else
if
constexpr
(
the_os
==
OS
::
Mac
)
{
//do something Appley
}
else
if
constexpr
(
the_os
==
OS
::
Windows
)
{
//do something Windowsy
}
//do something general
}
|
代码变得更清爽了,再也不需要像以前一样写#ifdef那样难看的代码块了。
constexpr lambda
constexpr lambda其实很简单,它的意思就是可以在constexpr 函数中用lambda表达式了,这在C++17之前是不允许的。这样使用constexpr函数和普通函数没多大区别了,使用起来非常舒服。下面是constexpr lambda的例子:
1
2
3
4
5
|
template
<
typename
I
>
constexpr
auto
func
(
I
i
)
{
//use a lambda in constexpr context
return
[
i
]
(
auto
j
)
{
return
i
+
j
;
}
;
}
|
constexpr if和constexpr lambda是C++17提供的非常棒的特性,enjoy it.
string_view
string_view的基本用法
C++17中的string_view是一个char数据的视图或者说引用,它并不拥有该数据,是为了避免拷贝,因此使用string_view可以用来做性能优化。你应该用string_view来代替const char和const string了。string_view的方法和string类似,用法很简单:
1
2
3
4
5
6
7
8
|
const
char
*
data
=
"test"
;
std
::
string_view
str1
(
data
,
4
)
;
std
::
cout
<<
str1
.
length
(
)
<<
'\n'
;
//4
if
(
data
==
str1
)
std
::
cout
<<
"ok"
<<
'\n'
;
const
std
::
string
str2
=
"test"
;
std
::
string_view
str3
(
str2
,
str2
.
size
(
)
)
;
|
构造string_view的时候用char*和长度来构造,这个长度可以自由确定,它表示string_view希望引用的字符串的长度。因为它只是引用其他字符串,所以它不会分配内存,不会像string那样容易产生临时变量。我们通过一个测试程序来看看string_view如何来帮我们优化性能的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
using
namespace
std
::
literals
;
constexpr
auto
s
=
"it is a test"
sv
;
auto
str
=
"it is a test"
s
;
constexpr
int
LEN
=
1000000
;
boost
::
timer
t
;
for
(
int
i
=
0
;
i
<
LEN
;
++
i
)
{
constexpr
auto
s1
=
s
.
substr
(
3
)
;
}
std
::
cout
<<
t
.
elapsed
(
)
<<
'\n'
;
t
.
restart
(
)
;
for
(
int
i
=
0
;
i
<
LEN
;
++
i
)
{
auto
s2
=
str
.
substr
(
3
)
;
}
std
::
cout
<<
t
.
elapsed
(
)
<<
'\n'
;
//output
0.004197
0.231505
|
我们可以通过字面量””sv来初始化string_view。string_view的substr和string的substr相比,快了50多倍,根本原因是它不会分配内存。
string_view的生命周期
由于string_vew并不拥有锁引用的字符串,所以它也不会去关注被引用字符串的生命周期,用户在使用的时候需要注意,不要将一个临时变量给一个string_view,那样会导致string_view引用的内容也失效。
1
2
3
4
5
|
std
::
string_view
str_v
;
{
std
::
string
temp
=
"test"
;
str_v
=
{
temp
}
;
}
|
这样的代码是有问题的,因为出了作用域之后,string_view引用的内容已经失效了。
总结
本文介绍了C++17的fold expression、constexpr if、constexpr lambda和string_view。fold expression为了简化可变模板参数的展开,让可以模板参数的使用变得更简单直观;constexpr if让模板具备if-else功能,非常强大。它也避免了写冗长的enable_if代码,让代码变得简洁易懂了;string_view则是用来做性能优化的,应该用它来代替const char*和const string。 这些特性对之前的C++14和C++11做了改进和增强,非常酷。
作者:祁宇,《深入应用C++11》作者,C++开源社区purecpp.org创始人,致力于C++11/14的应用、研究和推广。乐于研究和分享技术,爱好C++,爱好开源。