编程语言 cpp C++17 Standards (下) 星野暗涌 2026-05-08 2026-07-04 1. 新的求值顺序 🌐参考网站资料:
在C++17之前,很多表达式的求值顺序都是未指定的(unspecified),这意味着编译器可以自由选择求值顺序,这导致了不确定性和潜在的bug, C++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 #include <iostream> void process (int a, int b) { std::cout << "a = " << a << ", b = " << b << std::endl; } int main () { int x = 0 ; process (x++, x++); int i = 0 ; int j = 0 ; i = i++; std::map<int , int > m = { {1 , 10 }, {2 , 20 } }; auto it = m.begin (); int value = it++->second; 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 () { std::optional<int > maybeInt; std::optional<std::string> maybeString = "Hello" ; std::optional<double > empty = std::nullopt ; if (maybeInt.has_value ()) { std::cout << "has_value1" << std::endl; } if (maybeString) { std::cout << "has_value2" << *maybeString << std::endl; } 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 (); } 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 { } }; } int main () { test_example1 (); test_example2 (); return 0 ; }
3. std::variant 🌐网页资料:
std::varaint是C++17标准库加入的一个类模板,它代表了一个类型安全的联合体(union).它可以持有其模板参数列表中指定的任何一种类型的值
传统的联合体的问题:C风格或者是C++的普通的unoin不是类型安全的。你需要自己记住当前存储的是哪些类型,如果访问错了,(比如在一个存储了int的union上读取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> int main () { std::variant<int , double , std::string> v; v = 42 ; std::cout << "int: " << std::get <int >(v) << std::endl; v = 3.14 ; std::cout << "double: " << std::get <double >(v) << std::endl; v = "hello" ; std::cout << "string: " << std::get <std::string>(v) << std::endl; std::cout << "Current index: " << v.index () << std::endl; std::variant<std::string, std::string> v2; }
访问值 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" ; 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::visitstd::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> using value_t = std::variant<int , double , std::string>;struct VisitorOP { void operator () (int i) const { std::cout << "int: " << i << 'n'; } void operator () (double d) const { std::cout << "double: " << d << 'n'; } void operator () (const std::string& s) const { std::cout << "string: " << s << 'n'; } }; template <class ... Ts>struct overloaded : Ts... { using Ts::operator () ... ; };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 << 'n'; for (auto & v : vec) { std::visit ([](auto && arg) { std::cout << arg; }, v); value_t w = std::visit ([](auto && arg) -> value_t { return arg + arg; }, v); 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 << 'n'; else if constexpr (std::is_same_v<T, double >) std::cout << "double with value " << arg << 'n'; else if constexpr (std::is_same_v<T, std::string>) std::cout << "std::string with value " << std::quoted (arg) << 'n'; else static_assert (false , "non-exhaustive visitor!" ); }, w); } std::cout << 'n'; for (auto & v : vec) { std::visit (overloaded{ [](int arg) { std::cout << arg << ' '; }, [](double arg) { std::cout << arg << ' '; }, [](const std::string& arg) { std::cout << arg << ' '; } }, 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 () { std::any a1 = 42 ; std::any a2 = 3.14 ; std::any a3 = std::string ("Hello" ); std::any a4; a4 = std::pair <std::string, std::string>("xxxx" , "yyyy" ); std::cout << sizeof (a1) << 'n'; std::cout << sizeof (a4) << 'n'; a3. emplace <std::string>("World" ); 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> 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) << 'n'; } else if (item.type () == typeid (double )) { std::cout << " 浮点配置 : " << std::any_cast <double >(item) << 'n'; } else if (item.type () == typeid (std::string)) { std::cout << "字符串配置: " << std::any_cast <std::string&>(item) << 'n'; } else { assert (false ); } } } int main () { std::any a1 = 42 ; std::any a2 = 3.14 ; std::any a3 = std::string ("Hello" ); try { int int_value = std::any_cast <int >(a1); std::cout << "Value: " << int_value << 'n'; double double_value = std::any_cast <double >(a1); } catch (const std::bad_any_cast& e) { std::cout << "Cast failed: " << e.what () << 'n'; } std::string str_ref1 = std::any_cast <std::string>(a3); str_ref1[0 ]++; std::cout << std::any_cast <std::string>(a3) << 'n'; std::string& str_ref2 = std::any_cast <std::string&>(a3); str_ref2[0 ]++; std::cout << std::any_cast <std::string&>(a3) << 'n'; std::string&& str_ref3 = std::any_cast <std::string&&>(move (a3)); str_ref3[0 ]++; std::cout << std::any_cast <std::string&>(a3) << 'n'; std::string str_ref4 = std::any_cast <std::string&&>(move (a3)); str_ref4[0 ]++; std::cout << std::any_cast <std::string&>(a3) << 'n'; if (auto ptr = std::any_cast <int >(&a1)) { std::cout << "Value via pointer: " << *ptr << 'n'; } else { std::cout << "Not an int or is empty.n" ; } anyVector (); }
std::any和std::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_view是C++17标准库引入的一个费用有的字符串视图类,他提供了一种轻量的方式来**查看一个已有的字符串(或者是字符串数组)**而无需赋值其内容。你可以将它看作是一个观察字符串的“文档”本省不需要管理内存
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 () { std::string_view sv1 ("Hello, world!" ) ; print (sv1); 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> void process_string (std::string_view sv) { } 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, ':'); std::cout << sv << std::endl; auto v = split (str + 9 , '/'); 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 #include <iostream> #include <chrono> #include <string> #include <string_view> std::string_view get_view () { std::string temp = "Temporary string" ; return temp; } void error_example1 () { std::string_view sv = get_view (); std::cout << sv << std::endl; } void error_example2 () { char buffer[] = { 'T', 'e', 's', 't', '.', 't', 'x', 't' }; 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; str[0 ] = 'h'; std::cout << sv << std::endl; str = "New value 111111111111111111111111" ; std::cout << sv << std::endl; } int main () { error_example1 (); error_example2 (); error_example3 (); return 0 ; }