C++17 Standards (下)

1. 新的求值顺序

🌐参考网站资料:

  • C++17之前,很多表达式的求值顺序都是未指定的(unspecified),这意味着编译器可以自由选择求值顺序,这导致了不确定性和潜在的bugC++17的求值顺序规则大大提高了代码的可预测性和安全性,消除了许多历史遗留下来的未定义行为问题。

  • 建议还是不要依赖未指定的求值顺序,即使C++17修复了一些问题,最好还是写出不依赖特定求值顺序的代码,否则代码的可维护性和可读性都会变差,建议将复杂的表达式拆分为多个简单的语句。

表达式类型 C++17之前 C++17之后
a = b 未指定 先求值b,在求值a
a += b, a -=b 未定义 先求值b, 在求值a
a << b, a >> b 未指定 从左到求值
a[b] 未定义 先求值a, 在求值b
a → b 未定义 先求值a, 再求值b
f(a, b, c) 未定义 参数求值顺序仍然未指定,但都在函数调用之前✅
new Type(a, b) 未定义 先求职所有参数,在分配内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// import std;
#include <iostream>
void process(int a, int b) {
std::cout << "a = " << a << ", b = " << b << std::endl;
}
int main() {
int x = 0;
// C++17 前:未指定⾏为!
process(x++, x++); // 可能输出(0, 1)或(1, 0)
int i = 0;
int j = 0;

// C++17 前:未定义⾏为!
// C++17 后:明确先求值右边(i++),再求值左边(i)
i = i++; // 现在明确:i = 1
std::map<int, int> m = { {1, 10}, {2, 20} };
auto it = m.begin();

// C++17 前:未定义⾏为!it++和it->second的求值顺序不确定
// C++17 后:明确先求值it->second,再求值it++
int value = it++->second; // 安全:value = 10, it指向第⼆个元素
std::cout << value << std::endl;
return 0;
}

2. std::optional

什么是 std::optional

🌐网站资料介绍:

std::optional<T>是一个类模板,它表示一个可能包含一个类型为T的值,也可能不包含任何的值(即为空状态).它是一种类型安全的方式,用来代替诸如**“返回特殊值(-1,** **nullptr, EOF**)”或者是“输出型参数”等传统模式。**std::optional**一个简单却极其有用的工具,它极大地提高了代码的可读性和安全性。

为什么需要std::optional

std::optional解决了这些问题,他将值(或有或无)包装在一个类型中,强调调用者处理

常见接口

特性 说明 代码示例
创建空 表示无值 std::optioanal<int>empty; auto empty = std::nullpot
创建有值 包装一个值 std::optional<int> opt = 5; auto opt = std::make_optional(5)
检查 判断是否包含值 if(opt.has_value()){ ... } if(opt){ ... }
安全取值 有值返回值,无值抛异常 int x = opt.value()
安全取值(带默认) 无值时返回默认值 int x = opt.value_or(0);
不安全取值 使其变为空 opt.reset(); opt = std::nullopt;

下面我们那我就结合文档,写写示例代码:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
void test_example1()
{
// 1、定义optional对象

std::optional<int> maybeInt;
// 初始为空

std::optional<std::string> maybeString = "Hello"; // 初始有值

std::optional<double> empty = std::nullopt; // 显式设置为空

// 2、检查是否有值

if (maybeInt.has_value()) {
std::cout << "has_value1" << std::endl;
}
// 或者更简洁的写法

if (maybeString) {
std::cout << "has_value2" << *maybeString << std::endl;
}
//访问值

// 安全访问-⽆值时抛出
//std::bad_optional_access
try {
int value = maybeInt.value();
}
catch (const std::bad_optional_access& e) {
std::cout << e.what() << std::endl;
}
maybeInt = 1;
// 不安全但快速的访问-⽆值时⾏为未定义

int value1 = *maybeInt;
// 带默认值的访问

int value2 = maybeInt.value_or(2); // ⽆值时返回
std::cout << value2 << std::endl;
// 、修改值

maybeInt = 42;
// 赋新值

maybeInt = std::nullopt; // 设为空

maybeInt.reset();
}
// 设为空

// optional实践中的使⽤场景

void test_example2()
{
// 查找⼀个顶点对应的下标编号

std::map<std::string, int> indexMap = { {"张庄",1}, {"王村",2}, {"李家村",3}, {"王家坪",3} };
auto findIndex = [&indexMap](const std::string& str)->std::optional<int>
{
auto it = indexMap.find(str);
if (it != indexMap.end())
{
return it->second;
}
else
{
return std::nullopt;
}
};
std::string x;
std::cin >> x;
std::optional<int> index = findIndex(x);
if (index)
{
std::cout << x << "对应的编号为:" << *index << std::endl;
}
else
{
std::cout << x << "是⾮法顶点" << std::endl;
}
std::vector<std::string> v = { "张庄", "李庄", "" };
auto access = [&v](int i)-> std::optional<std::string>
{
if (i < v.size())
{
return v[i];
}
else
{
// 以前的语法这⾥函数只能⽤抛异常或者断⾔⽅式处理i越界的情况
// i越界不能返回"",因为正常数据可能就是"",⽆法区分std::nullopt;
}
};
}

int main()
{
test_example1();
test_example2();
return 0;
}

3. std::variant

🌐网页资料:

std::varaintC++17标准库加入的一个类模板,它代表了一个类型安全的联合体(union).它可以持有其模板参数列表中指定的任何一种类型的值

传统的联合体的问题:C风格或者是C++的普通的unoin不是类型安全的。你需要自己记住当前存储的是哪些类型,如果访问错了,(比如在一个存储了intunion上读取float会导致未定义行为,而且他无法处理非平凡类型(如std::string)。std::variant 的优势是解决了这些所有问题。他自己知道当前存储的是哪种类型,确保对象被正确的构造和析构,我们可以把它想象成一个**“智能的、类型丰富的”****union**

定义修改和赋值

我们直接看下面的代码,展示一下基本的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <variant>
#include <string>
#include <iostream>
//union
//⽰例1:定义和赋值

int main() {
// 定义⼀个variant,它可以存储⼀个int,⼀个double,或⼀个std::string
std::variant<int, double, std::string> v;
v = 42; // 现在持有int
std::cout << "int: " << std::get<int>(v) << std::endl;
v = 3.14; // 现在持有double
std::cout << "double: " << std::get<double>(v) << std::endl;
v = "hello"; // 现在持有std::string
std::cout << "string: " << std::get<std::string>(v) << std::endl;
// std::cout << "int 2 : " << std::get<int>(v) << std::endl; // 报错,现在variant持有的时std::string类型,variant在一个状态下只能存储一种类型的值!
// 赋值时如果找不到对应类型的值则报错
// v = std::pair<int, int>{}; // Error
// 访问值使⽤index()获取当前持有的类型索引
std::cout << "Current index: " << v.index() << std::endl;
std::variant<std::string, std::string> v2;
//v2 = "abc"; // Error
}

访问值

std::get<Type/Index>

使用std::variant<Type>或者是std::get<index>你可以通过类型或者是索引来直接获取值。但是如果当前variant存储的不是你请求的类型或者是索引,他就会抛异常std::bad_variant_access

1
2
3
4
5
6
7
8
9
10
11
int main(){
std::variant<int, double> v = 42;
try{
std::cout << std::get<int>(v) << std::endl;
std::cout << std::get<0>(v) << std::endl;
std::cout << std::get<double>(v) << std::endl; // 抛出异常
}catch(const std::bad_variant_access& e){
std::cout << "Error: " << e.what() << std::endl;
}
return 0;
}

std::get_if<Type>

  • std::gte_if<Type> 不会抛出异常。他接受一个指针参数,如果**variant**当前存储的是制定类型,则返回一个执行该值的指针;否则返回**nullptr**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(){
std::variant<int, double, std::string> v = "hello";

// 使用std::get_if尝试获取值
if (auto pval = std::get_if<int>(&v)){
std::cout << "int value: " << *pval << endl;
}
else if (auto pval = std::get_if<double>(&v)){
std::cout << "double value: " << *pval << std::endl;
}
else if (auto pval = std::get_if<std::string>(&v)){
std::cout << *pval << std::endl;
}
}

std::visit

std::visit是最安全和强大的方式,所以也是最推荐的方式,std::visit允许你提供一个访问者(visitor)来根据当前存储的类型执行相关的操作,这个是类型安全、最为清晰的方式。第一个参数访问这是一个可调用对象,通常是重载了一个operator()的类(或者是使用**lambda**表达式结合**overloaded**技巧),std::visit会把std::variant 对象的值取出来,作为参数传给visitor可调用处理

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iomanip>
#include <iostream>
#include <string>
#include <type_traits>
#include <variant>
#include <vector>

// the variant to visit
using value_t = std::variant<int, double, std::string>;
struct VisitorOP {
void operator()(int i) const {
std::cout << "int: " << i << &#x27;n&#x27;;
}
void operator()(double d) const {
std::cout << "double: " << d << &#x27;n&#x27;;
}
void operator()(const std::string& s) const {
std::cout << "string: " << s << &#x27;n&#x27;;
}
};
// helper type for the visitor #4
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

int main()
{
std::vector<value_t> vec = { 10, 1.5, "hello" };
for (auto& v : vec)
{
std::visit(VisitorOP(), v);
}
std::cout << &#x27;n&#x27;;
for (auto& v : vec)
{
// 1. void visitor, only called for side-effects (here, for I/O)
std::visit([](auto&& arg) { std::cout << arg; }, v);
// 2. value-returning visitor, demonstrates the idiom of returning another variant
value_t w = std::visit([](auto&& arg) -> value_t { return arg + arg;
}, v);
// 3. type-matching visitor: a lambda that handles each type differently
std::cout << ". After doubling, variant holds ";
std::visit([](auto&& arg)
{
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>)
std::cout << "int with value " << arg << &#x27;n&#x27;;
else if constexpr (std::is_same_v<T, double>)
std::cout << "double with value " << arg << &#x27;n&#x27;;
else if constexpr (std::is_same_v<T, std::string>)
std::cout << "std::string with value " << std::quoted(arg)
<< &#x27;n&#x27;;
else
static_assert(false, "non-exhaustive visitor!");
}, w);
}
std::cout << &#x27;n&#x27;;
for (auto& v : vec)
{
// 4. another type-matching visitor: a class with 3 overloaded operator()&#x27;s
std::visit(overloaded{
[](int arg) { std::cout << arg << &#x27; &#x27;; },
[](double arg) { std::cout << arg << &#x27; &#x27;; },
[](const std::string& arg) { std::cout << arg << &#x27; &#x27;; }
}, v);
}
}

4. std::any

🌐资料网站:

std::any是一个可以存储任意类型**(必须时可拷贝构造的)单个值的容器。**当你从any中取出值,你必须知道其原始的类型,并通过std::any_cast进行安全得转换。如果类型不匹配会抛出异常或者是返回空指针。

  • 与原始得void*不同,any会记录类型信息,并在any_cast时进行检查,其次他们管理着自己内部的对象生命周期(构造、拷贝、析构),为了避免为小对象进行频繁的内存堆栈分配,许多实现会使用一个小缓存区优化(SBO, Small Buffer Optimization), MSVC下的std::string就使用了这个优化。

  • std::any的接口非常的简介,主要是使用的函数:

函数 作用
构造函数 std::any any_value = 42; std::any any_value = std::string("hello");
operator= 可以赋值任意类型修改
**emplace<T>(args...)** 原地构造一个类型为T的对象,参数args传递给T的构造函数
reset() 销毁内部包含的对象,使any变为空
**has_value()** 返回一个bool 值,判断any对象是否有值
type() 返回一个std::typw_info const&,表示当前包含值得类型。如果any为空,则返回typeid(void)
std::any_cast<T> 最重要的函数!用于从any对象中提取值。如果转换失败,会抛出std::bad_any_cast异常

构造和赋值

现在我们看一下这段示例代码:

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
#include <any>
#include <string>
#include <iostream>

int main() {
// 存放int
std::any a1 = 42;
// 存放double
std::any a2 = 3.14;

std::any a3 = std::string("Hello"); // 存放std::string
std::any a4;
a4 = std::pair<std::string, std::string>("xxxx", "yyyy");


std::cout << sizeof(a1) << &#x27;n&#x27;;
std::cout << sizeof(a4) << &#x27;n&#x27;;
// 检查是否有值
// 使⽤emplace原地构造
a3.emplace<std::string>("World"); // 现在a3包含"World",之前的"Hello"被销毁!
if (a3.has_value()) {
std::cout << "a has value" << std::endl;
// 获取类型信息
const std::type_info& ti = a1.type();
std::cout << "a type: " << ti.name() << std::endl;
std::cout << std::any_cast<std::string>(a3) << std::endl;
}
}

any_cast的三种取值方式

🌐

这里我们直接看看下面的代码,聪明如你相信很快就可以明白any_cast<T>的使用方法

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <any>
#include <string>
#include <vector>
#include <assert.h>
// vector中存储any类型

void anyVector() {
std::string str("hello world");
std::vector<std::any> v = { 1.1, 2, str };
for (const auto& item : v) {
if (item.type() == typeid(int)) {
std::cout << "整数配置: " << std::any_cast<int>(item) << &#x27;n&#x27;;
}
else if (item.type() == typeid(double)) {
std::cout << " 浮点配置 : " << std::any_cast<double>(item) << &#x27;n&#x27;;
}
else if (item.type() == typeid(std::string)) {
std::cout << "字符串配置: " << std::any_cast<std::string&>(item) << &#x27;n&#x27;;
}
else
{
assert(false);
}
}
}
int main() {
std::any a1 = 42; // 存放int
std::any a2 = 3.14; // 存放double
std::any a3 = std::string("Hello"); // 存放 std::string

// ⽅式⼀:转换为值的类型(如果类型不匹配,抛出std::bad_any_cast)

try {
int int_value = std::any_cast<int>(a1); // 正确,a1存放的是int
std::cout << "Value: " << int_value << &#x27;n&#x27;;
double double_value = std::any_cast<double>(a1); // 错误!抛出异常

}
catch (const std::bad_any_cast& e) {
std::cout << "Cast failed: " << e.what() << &#x27;n&#x27;;
}
// ⽅式⼆:转换为值和转换为值的引⽤

// 这⾥any_cast返回的是a3存储对象的拷⻉,要尽量避免这样使⽤

std::string str_ref1 = std::any_cast<std::string>(a3);
str_ref1[0]++;
std::cout << std::any_cast<std::string>(a3) << &#x27;n&#x27;;
std::string& str_ref2 = std::any_cast<std::string&>(a3);
str_ref2[0]++;
std::cout << std::any_cast<std::string&>(a3) << &#x27;n&#x27;;
std::string&& str_ref3 = std::any_cast<std::string&&>(move(a3));
str_ref3[0]++;
std::cout << std::any_cast<std::string&>(a3) << &#x27;n&#x27;;
std::string str_ref4 = std::any_cast<std::string&&>(move(a3));
str_ref4[0]++;
std::cout << std::any_cast<std::string&>(a3) << &#x27;n&#x27;;
// ⽅式三:转换为指针(如果类型不匹配,返回nullptr,不会抛出异常)

if (auto ptr = std::any_cast<int>(&a1)) { // 传递指针,返回指针

std::cout << "Value via pointer: " << *ptr << &#x27;n&#x27;;
}
else {
std::cout << "Not an int or is empty.n";
}
anyVector();
}

std::anystd::variant对比

🧭对比

  • 功能角度,它们都是用于储存多种不同类型的类型安全的单值容器,使用方法上也有诸多相似

  • 使用角度:**std::variant**在编译时就知道了所有已知的类型。**std::any****运行时才知道具体的类型。他们都可以在构造初始化或者是赋值。**底层自动管理,非常的简单,访问值得时候:**std::any**需要使用**std::any_cast**进行转换, **std::variant**通过**std::get****std::vist**访问器访问。

  • 底层角度:std::varaint直接存储对象std::any小对象存储在对象中,大对象存储在堆上,所以**std::any**存储对象得成本会更高一些。**std::varaint**使用**std::visit**访问通常被表面一起优化为一个高效的跳表。而**std::any**需要访问运行时类型查询或尝试转换,者通过一系列的if_else进行比较会比较麻烦,比**std::varaint**得跳转表慢。所以存储和访问角度std::varaint的效率都会更高一些一般情况下建议使用**std::varaint**除非是一些可能存储的类型很多或者是无法确认需要存储的类型时使用**std::any**

5. std::string_view

🌐资料网站:

std::string_viewC++17标准库引入的一个费用有的字符串视图类,他提供了一种轻量的方式来**查看一个已有的字符串(或者是字符串数组)**而无需赋值其内容。你可以将它看作是一个观察字符串的“文档”本省不需要管理内存

  • std::string_viewstd::string拥有完全类似的接口,查看文档上我们也可以得知,他主要提供**string**读相关的接口,不提供写接口,另外他自身支持一些移动视图位置的接口。

  • std::string_view设计思路是非拥有空间和数据,底层不分配内存,只保留原始字符串的指针和长度; 无所有权,不负责字符串的生命周期管理;**const**视图只能观察,不能修改底层字符

std::string_view的构造方式

看看下面的代码,我们了解一下std::string_view的几种主要的构造方法:

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
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <string>
#include <string_view>

void print(std::string_view sv) {
std::cout << "String view: " << sv << "n";
std::cout << "Length: " << sv.size() << "n";
for (size_t i = 0; i < sv.size(); ++i)
{
std::cout << sv[i] << " ";
}
std::cout << "n";
for (auto ch : sv)
{
std::cout << ch << " ";
}
std::cout << "n";
}
int main() {
// 从C⻛格字符串构造

std::string_view sv1("Hello, world!");
print(sv1);
//从std::string构造

std::string str = "C++17 string_view";
std::string_view sv2(str);
print(sv2);
// 从部分字符串构造

std::string_view sv3(str.c_str() + 6, 6);
print(sv3);
// 从字⾯量构造

using namespace std::literals;
std::string_view sv4 = "Literal"sv;
print(sv4);
}

std::string_view的特点

  • std::string_view **几乎就是零开销,****他不会复制字符串,构造和析构的成本极低。**并且提供了与**std::string**相似的接口(几乎所有的读操作接口),兼容性强,可以高效的从**std::string**字符串字面量**char***等任何字符序列构造而来。这样的场景下可以减少很多的拷贝和构造,有可以利用和**string**相关的接口来处理。拷贝和构造,又可以利用**string**类似的接口来处理。
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
// 使⽤场景:const std::string &做形参,下⾯传字⾯量串会构造⼀个临时对象

// std::string_view做形参,下⾯不会构造临时对象。

// void process_string(const std::string& s)
void process_string(std::string_view sv)
{
// 处理字符串,⽆需关⼼原始类型

}
// 使⽤场景:解析字符串处理,使⽤std::string_view做为参数和返回值,零拷⻉

// 如果使⽤std::string那么在传参和传值返回都要构造对象
std::string_view extract_str(std::string_view input, char delimiter) {
size_t pos = input.find(delimiter);
return input.substr(0, pos);
}
// 继续切分字符串

std::vector<std::string_view> split(std::string_view str, char delimiter) {
std::vector<std::string_view> result;
size_t start = 0;
size_t end = str.find(delimiter);
while (end != std::string_view::npos) {
result.push_back(str.substr(start, end - start));
start = end + 1;
end = str.find(delimiter, start);
}
result.push_back(str.substr(start));
return result;
}
int main() {
// 可以接受各种字符串类型

process_string("C-string");
std::string s("std::string");
process_string(s);
const char* str = "https://chat.deepseek.com/a/chat/s/4ae60e97-d7b0-45aadd-5f82d76e74e7";
std::string_view sv = extract_str(str, &#x27;:&#x27;);
std::cout << sv << std::endl;
auto v = split(str + 9, &#x27;/&#x27;);

for (auto e : v)
{
std::cout << e << std::endl;
}
std::cout << std::endl;
return 0;
}
  • 在使用std::string_view的时候一定要注意生命周期管理,std::string_view对象的生命周期不要超过指向的**std::string**对象的声明周期

  • 不一定以空字符串**''** 终止sv.data()返回的指针不一定指向一个以'' 结尾的字符串。如果你需要传递一个期望C风格的字符串(''结尾)的API,这是不安全的。你必须确保视图本身以'' 结尾,或者是手动创建一个以'' 结尾的副本(例如使用std::string(sv));

下面我们就看一看示例代码,看看std::string_view可能存在的安全问题:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// string_view⼀些使⽤陷阱

#include <iostream>
#include <chrono>
#include <string>
#include <string_view>

std::string_view get_view() {
std::string temp = "Temporary string";
return temp; // 严重错误!temp将被销毁

}
void error_example1() {
std::string_view sv = get_view(); // sv现在指向已释放的内存

std::cout << sv << std::endl; // 未定义⾏为

}
void error_example2()
{
char buffer[] = { &#x27;T&#x27;, &#x27;e&#x27;, &#x27;s&#x27;, &#x27;t&#x27;, &#x27;.&#x27;, &#x27;t&#x27;, &#x27;x&#x27;, &#x27;t&#x27; }; // 不是空终⽌的

// 正确指定⻓度
std::string_view sv(buffer, 4);
std::cout << sv << std::endl;
std::string_view filename(buffer);
FILE* fout = fopen(filename.data(), "w");
if (fout != NULL)

{
fputs("fopen example", fout);
fclose(fout);
}
else
{
perror("fopen fail");
}
}
void error_example3()
{
std::string str = "Hello";
std::string_view sv = str;
// 修改原始字符串会使string_view失效

// 合法,sv仍然有效

str[0] = &#x27;h&#x27;;
std::cout << sv << std::endl;
str = "New value 111111111111111111111111"; // 可能导致重新分配,使sv失效

std::cout << sv << std::endl;
}
int main()
{
error_example1();
error_example2();
error_example3();
return 0;
}