C++17特性汇总

C++17特性汇总

这里只挑取一些比较感兴趣的话题

结构化绑定Structured Binding

  • 类/结构体:
1
2
3
4
5
6
7
8
9
10
11
struct B {
int a = 1;
int b = 2;
};
struct D1 : B {};
auto [x, y] = D1{}; // OK

struct D2 : B {
int c = 3;
};
auto [i, j, k] = D2{}; // 编译期ERROR
  • 原生数组:
1
2
int arr[] = { 47, 11 };
auto [x, y] = arr; // x和y是arr中的int元素的拷贝
  • std::pair,std::tuplestd::array 也可以

  • 为Tuple-like的类自定义:

    image-20240610165459961

​ 如果需要可写的,还需要再多定义几个返回值不是const限定的get。

内敛变量

首先:

  • C++不允许在类内对非常量静态变量进行初始化,这是因为如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。
  • C++可以在类定义的外部定义并初始化非常量静态成员,但注意如果被多个 CPP 文件同时包含的话又会引发新的错误,即违反ODR原则,预处理#pragma once也没用,因为编译顺序还是会影响了这个静态变量的值最后是什么。

比较好的一种实践是: .h定义类并类声明类静态变量,在.cpp中进行定义,即一个翻译单元里才拥有它。

就像内敛函数可以解决被多个cpp引用一样,C++提出了内敛变量,有了inline static 我们可以解决这个问题:

使用了 inline修饰符之后,即使定义所在的头文件被多个 CPP 文件包含,也只会有一个全局对象,inline帮助我们在多个翻译单元中可以重复定义,帮助我们突破了ODR规则。

1
2
3
4
5
class A
{
public:
inline static int a = 5; //从C++17后ok
};

C++17后constexpr static成员现在隐含inline:

1
2
3
4
5
class A
{
public:
constexpr static int a = 5; //从C++17后ok
};

注意事项:inline帮助我们在多个翻译单元中可以重复定义,帮助我们突破了ODR规则,但也会带来UB:

但是请注意它带来的一些UB行为,常常多个inline不同的初始值会导致UB

1
2
3
4
5
6
7
8
9
//1.cpp
inline int i = 2;//definition

//main.cpp
inline int i = 5;//definition
int main()
{
std::cout<<i<<std::endl; // output 2 or 5, it's ub
}

强制拷贝省略Mandatory Copy Elision

C++17引入了一个新的规则:当以值传递或返回一个临时对象的时候必须省略对该临时对象的拷贝。

在之前拷贝省略一直是一种优化手段,常常与之相关的技术叫做RVO/NRVO等,C++17中开始强制省略临时变量拷贝,也就是即使禁用拷贝构造都可以,这在之前是不可以的。

1
2
3
4
5
6
int main()
{
foo(MyClass{}); // 传递临时对象来初始 化param
MyClass x = bar(); // 使用函数返回的临时对象初始化x
foo(bar()); // 使用函数返回的临时对象初始化param
}

即使myclass的拷贝构造标为delete依然可以编译,这在C++17前是无法做到的。

有关Lambda

constexpr lambda

就像函数一样,constexpr在C++17也对lambda进行了支持:

1
2
3
constexpr auto foo = [](int a, int b)
{ return a + b; };
static_assert(6 == foo(2, 3), "compile-time judge"); //static_assert关键字,用来做编译期间的断言,因此叫做静态断言

向lambda传递this的拷贝

我们往往捕获this都是值捕获或是引用捕获,不过捕获的都是this指针,如果想获得this的拷贝:

在C++14可以使用广义lambda:

1
auto l1 = [thisCopy=*this] { std::cout << thisCopy.name << '\n'; };

C++17后可以这样,更简单了:

1
auto l1 = [*this] { std::cout << name << '\n'; };

std::as_const()常量引用捕获

1
auto printColl = [&coll = std::as_const(coll)]

constexpr if

编译期if分支

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
std::string asString(T x)
{
if constexpr(std::is_same_v<T, std::string>) {
return x; // statement invalid, if no conversion to string
}
else if constexpr(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
}
}

规定了求值顺序(带来的异常安全性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <unordered_map>
#include <cstdio>
int a() { return std::puts("a"); }
int b() { return std::puts("b"); }
int c() { return std::puts("c"); }
void z(int, int, int) {}

int main()
{
std::unordered_map<int, int> m_map;
m_map[0] = m_map.size(); // 此处不确定插入{0, 0},还是{0, 1}
std::cout << "m_map[0]:" << m_map[0] << std::endl;

z(a(), b(), c()); // all 6 permutations of output are allowed
return a() + b() + c(); // all 6 permutations of output are allowed
}
1
2
3
4
5
6
7
m_map[0]:0
c
b
a
a
b
c

为了解决类似问题,C++17优化了求值顺序:

  • 后缀表达式从左到右求值,包括函数调用成员选择表达式
  • 赋值表达式从右向左求值,包括复合赋值;
  • 从左到右计算移位操作符的操作数。

String作为模板参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<const char* str>
class Message {
...
};
extern const char hello[] = "Hello World!"; // external linkage
const char hello11[] = "Hello World!"; // internal linkage

void foo()
{
Message<hello> msg; // OK (all C++ versions)
Message<hello11> msg11; // OK since C++11
static const char hello17[] = "Hello World!"; // no linkage
Message<hello17> msg17; // OK since C++17
}

auto作为模板参数占位符

从C++17开始,你可以使用auto来声明一个非类型模板参数。比如:

1
2
3
template<auto N> class S {
...
};

这允许我们针对不同类型都可以实例化非类型模板参数N:

1
2
S<42> s1; // OK: type of N in S is int
S<'a'> s2; // OK: type of N in S is char

然而,对于那些规则不允许的类型作为模板类型,这个特性仍然是没用的,即不会实例化成功:

1
S<2.5> s3; // ERROR: template parameter type still cannot be double

折叠表达式

在C++17前,我们想要对C++11的参数包展开,可以进行递归展开,逗号表达式等方法,C++17后折叠表达式是一个很好的选择,可以对参数包进行操作。

递归展开参数包

在C++17前,我们想要对C++11的参数包展开,就需要递归展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//C++11,应对单参数的递归出口
void print() {
std::cout << std::endl;
}

template<typename T, typename... Ts>
void print(T arg, Ts... args) {
std::cout << arg << ", ";
print(args...);
}

int main() {
print("hello", "string", 1, 2, 3);
return 0;
}

即需要电一一个递归打印,和一个递归出口print。

用 逗号表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename T>
void print_item(T arg) {
std::cout << arg << ", ";
}

template<typename... T>
void expand(T... args) {

//1. 可以这样
int arr[] = { (print_item(args), 0)... }; //逗号表达式


//2. 也可以直接:
std::initializer_list<int>{ (print_item(args), 0)... };


std::cout << std::endl;
}

int main() {
expand("hello", "string", 1, 2, 3);
return 0;
}

回顾一下逗号表达式a = (b = c, d),表达式的执行顺序是,先进行b = c的赋值,接着括号中的逗号表达式返回d,因此最终表达式a等于d

所以上面的例子中,int arr[] = { (print_item(args), 0)... };语句,其实核心部分是这一句(print_item(args), 0),先指定print_item()函数,然后逗号表达式返回0;然后利用初始化列表(initialize list)来初始化一个初值都是0、大小为sizeof...(args)的数组。通过初始化列表,最终展开为

1
2
3
4
5
6
int arr[] = {
(print_item(args0), 0),
(print_item(args1), 0),
(print_item(args2), 0),
...
};

C++17后用fold expression

有四种语法

1
2
3
4
5
6
7
(pack op ...) //一元右折叠

(... op pack) //一元左折叠

(pack op ... op init) //二元右折叠

(init op ... op pack) //二元左折叠

一元折叠

  • 一元右折叠

形如(E op ...)的折叠表达式称为一元右折叠。

一元右折叠展开之后的含义是(E1 op (... op (En-1 op En)))

  • 一元左折叠

形如(... op E)的折叠表达式称为一元左折叠。

一元左折叠展开之后的含义是(((E1 op E2) op ...) op En)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//一元右折叠
template<typename... T>
auto sumR(T... args) {
return (args + ...);
}

//一元左折叠
template<typename... T>
auto sumL(T... args) {
return (... + args);
}

int main() {
std::cout << sumR(1, 2, 3, 4, 5) << std::endl; //15
std::cout << sumL(1, 2, 3, 4, 5) << std::endl; //15
return 0;
}
//sumR(1, 2, 3, 4, 5)右折叠展开后等价于1 + (2 + (3 + (4 + 5)));
//sumL(1, 2, 3, 4, 5)左折叠展开后等价于(((1 + 2) + 3) + 4) + 5

一元折叠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//一元右折叠
template<typename... T>
void printR(T... args) {
((std::cout << args << ", "), ...) << std::endl;
}

//一元左折叠
template<typename... T>
void printL(T... args) {
(..., (std::cout << args << ", ")) << std::endl;
}

int main() {
printR("hello", "zhxilin", 1, 2, 3);
printL("hello", "zhxilin", 1, 2, 3);
//输出结果一样
return 0;
}

在上面这个例子的折叠表达式中,op是逗号,E是输出语句std::cout << args << ", ",所以其实是一个逗号表达式的折叠表达式。注意E语句中<<操作符的返回结果是std::ostream,所以可以继续输出流操作。

二元折叠

二元折叠分为二元右折叠和二元左折叠。

假设表达式是E(包含参数包),操作符是op,初值是I

  • 二元右折叠

形如(E op ... op I)的折叠表达式称为二元右折叠。

二元右折叠展开之后的含义是(E1 op (... op (En-1 op (En op I))))

  • 二元左折叠

形如(I op ... op E)的折叠表达式称为二元左折叠。

二元左折叠展开之后的含义是((((I op E1) op E2) op ...) op En)

再看一下前面通过一元折叠表达式实现的累加函数,这种实现已经可以满足大多数情况了,但是有一种情况没法满足,就是函数传递了空参数。如果非要用一元折叠表达式实现,且支持传递空参数,那么需要对模板进行特化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//一元右折叠
template<typename... T>
auto sumR(T... args) {
return (args + ...);
}
auto sumR() { return 0; }

//一元左折叠
template<typename... T>
auto sumL(T... args) {
return (... + args);
}
auto sumL() { return 0; }

int main() {
std::cout << sumR() << std::endl; //0
std::cout << sumL() << std::endl; //0
return 0;
}

虽然可行,但是还需要实现特化版本,还是比较麻烦的。更简单的方式是使用二元折叠表达式给定初值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//二元右折叠表达式
template<typename... T>
auto sumR(T... args) {
return (args + ... + 0);
}

//二元左折叠表达式
template<typename... T>
auto sumL(T... args) {
return (0 + ... + args);
}

int main() {
std::cout << sumR() << std::endl; //0
std::cout << sumL() << std::endl; //0
std::cout << sumR(1, 2, 3, 4, 5) << std::endl; //15
std::cout << sumL(1, 2, 3, 4, 5) << std::endl; //15
return 0;
}

如此一来,只需要定义一个模板函数即可,不需要定义特化版本,就可以实现传空参数的累加求和了。

std::string_view

​ C++17中我们可以使用std::string_view来获取一个字符串的视图,字符串视图并不真正的创建或者拷贝字符串,而只是拥有一个字符串的查看功能。std::string_view比std::string的性能要高很多,因为每个std::string都独自拥有一份字符串的拷贝,而std::string_view只是记录了自己对应的字符串的指针和偏移位置。当我们在只是查看字符串的函数中可以直接使用std::string_view来代替std::string。

​ 比如,最常见的substr,string_view效率会高很多。

std::variant<>

可以视为拥有类型安全的union。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <variant>
#include <iostream>
int main()
{
std::variant<int, std::string> var{"hi"}; // 初 始 化 为string选 项
std::cout << var.index() << '\n'; //1
var = 42; // 现 在 持 有int选 项
std::cout << var.index() << '\n'; //0
...
try {
int i = std::get<0>(var);
std::string s = std::get<std::string>(var); // 通过类型访问(这里会抛出异常)
...
}
catch (const std::bad_variant_access& e) { // 当索引/类型错误时进行处理
std::cerr << "EXCEPTION: " << e.what() << '\n';
...
}
}

std::any

  • any_cast来转换类型
  • 使用type()成员函数要注意是否开启了rtti
1
2
3
4
5
6
7
8
9
10
11
std::any a; // a为空
std::any b = 4.3; // b有类型为double的值4.3
a = 42; // a有类型为int的值42
b = std::string{"hi"}; // b有类型为std::string的值"hi"
if (a.type() == typeid(std::string)) { //需要rtti支持
std::string s = std::any_cast<std::string>(a);
useString(s);
}
else if (a.type() == typeid(int)) {
useInt(std::any_cast<int>(a));
}

std::optional<>

std::optional<>塑造了一个可容纳类型的、可空的对象。

比如:middle name 可以为空的一个场景

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
27
28
class Name
{
private:
std::string first;
std::optional<std::string> middle;
std::string last;
public:
Name (std::string f,
std::optional<std::string> m,
std::string l)
: first{std::move(f)}, middle{std::move(m)}, last{std::move(l)} {
}
friend std::ostream& operator << (std::ostream& strm, const Name& n) {
strm << n.first << ' ';
if (n.middle) {
strm << *n.middle << ' ';
}
return strm << n.last;
}
};

int main()
{
Name n{"Jim", std::nullopt, "Knopf"};
std::cout << n << '\n';
Name m{"Donald", "Ervin", "Knuth"};
std::cout << m << '\n';
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!