c++17特性
1.关键字
1. auto关键字
从c++11开始,auto关键字能够通过初始化器推导出变量的类型。在c++14中,auto关键字的能力进一步提升,能够通过return语句推导出函数的返回类型。 使用auto关键字能够提高编码效率,同时能够简化重构流程。但是,C++11中的auto推导,往往结果与预期的不同。
c++11 中为了支持统一初始化,引入了新的统一初始化语法,如下所示。
// c++11
auto x3{ 1, 2 }; // std::initializer_list<int>
auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int>
auto x5{ 3 }; // std::initializer_list<int>
这三种方式初始化的变量,最终类型推导的结果都是 std::initializer_list , 而不是我们认为的int。 这是因为 当用于auto声明变量的表达式是{}括起来的,推导的型别就会变成 std::initializer_list。
在C++17中,对auto表达式推导的规则进行了改变
// c++17
auto x3{ 1, 2 }; // error: not a single element
auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int>
auto x5{ 3 }; // decltype(x5) is int
对比发现, auto x5{3}, 会直接将变量推导成 x5, 而 x3{1, 2} 这种方式也会编译失败。auto推导的规则变得更加直观。
2. lambda表达式
- lambda也是c++11中引入的,在C++11中,lambda表达式只能用捕获this,this是当前对象的一个只读的引用。
- 在C++17中,可以捕获*this, *this是当前对象的一个拷贝,捕获当前对象的拷贝,能够确保当前对象释放后, lambda表达式能安全的调用this中的变量和方法。
3. inline变量
Inline 变量, inline变量可以让变量有多于一次的定义。C++17之前,我们定义全局变量, 总需要将变量定义在cpp文件中,然后在通过extern关键字来告诉编译器 这个变量已经在其他地方定义过了。 inline变量出现后,我们可以直接将全局变量定义在头文件中,而不用担心出现redefine的错误信息。
4.constexpr
扩展constexpr使用范围,可用于if语句中,也可用于lambda表达式中。
template<bool ok>
constexpr void foo()
{
//在编译期进行判断,if和else语句不生成代码
if constexpr (ok == true)
{
//当ok为true时,下面的else块不生成汇编代码
std::cout << "ok" << std::endl;
}
else
{
//当ok为false时,上面的if块不生成汇编代码
std::cout << "not ok" << std::endl;
}
}
int main()
{
foo<true>();//输出ok,并且汇编代码中只有std::cout << "ok" << std::endl;这一句
foo<false>();//输出not ok,并且汇编代码中只有std::cout << "not ok" << std::endl;这一句
return 0;
}
5.static_assert
扩展static_assert用法,静态断言的显示文本可选。
static_assert(true, "");
static_assert(true);//c++17支持
2.语法
1.折叠表达式
2.结构化绑定
用一对包含一个或多个变量的中括号,表示结构化绑定,但是使用结构化绑定时,须用auto关键字,即绑定时声明变量
/*
* 例子:多值返回
*/
struct S
{
double num1;
long num2;
};
S foo(int arg1, double arg2)
{
double result1 = arg1 * arg2;
long result2 = arg2 / arg1;
return {result1, result2};//返回结构体S对象
};
int main()
{
auto [num1, num2] = foo(10, 20.2);//自动推导num1为double,num2为long
return 0;
}
#include<list>
#include<map>
/*
* 例子:循环遍历
*/
template<typename T, typename U>
struct MyStruct
{
T key;
U value;
};
int main()
{
std::list<MyStruct<int, double>> Container1;
std::map<int, MyStruct<long long, char>> Container2;
for(auto [key, value] : Container1)
{
//key为int类型,value为double类型
}
for(auto [key, value] : Container2)
{
//key为int类型,value为MyStruct<long long, char>类型
//value1为long long类型,value2为char类型
auto [value1, value2] = value;
}
return 0;
}
3. 条件表达式中支持初始化语句
c++17中支持在 if 或者switch 语句中进行初始化, 这个能力的出现能够让代码更加的简洁。
// c++17之前
map<int, string> c = {{1,"a"}};
{
auto res = c.insert(make_pair(2, "b"));
if(!res.second) {
cout << "key 1 exist" << endl;
} else {
cout << "insert success, value:" << res.first->second << endl;
}
}
上面的一段代码,由于res是一个临时变量,不想影响到后面的代码,所以用一对花括号限制了其作用域。但是如果使用c++17的语法, 在if条件中初始化res,则代码就会显得更加简洁
// c++17
map<int, string> c = {{1,"a"}};
if(auto res = c.insert(make_pair(2, "b")); !res.second ) {
cout << "key 1 exist" << endl;
} else {
cout << "insert success, value:" << res.first->second << endl;
}
template<long value>
void foo(int &ok)
{
if constexpr (ok = 10; value > 0)
{
}
}
int main()
{
int num = 0;
if(int i = 0; i == 0)
{
}
foo<10>(num);
switch(int k = 10; k)
{
case 0:break;
case 1:break;
default:break;
}
return 0;
}
4 聚合初始化
在初始化对象时,可用花括号进行对其成员进行赋值
struct MyStruct1
{
int a;
int b;
};
struct MyStruct2
{
int a;
MyStruct1 ms;
};
int main()
{
MyStruct1 a{10};
MyStruct2 b{10, 20};
MyStruct2 c{1, {}};
MyStruct2 d{{}, {}};
MyStruct2 e{{}, {1, 2}};
return 0;
}
5.嵌套命名空间
//传统写法
namespace A
{
namespace B
{
namespace C
{
};
};
};
//新写法
namespace A::B::C
{
};
3 宏
3.1 __has_include
判断有没有包含某文件
如:
int main()
{
#if __has_include(<cstdio>)
printf("hehe");
#endif
#if __has_include("iostream")
std::cout << "hehe" << std::endl;
#endif
return 0;
}
4.数据类型
c++17的标准库也进行了扩充, 新增了下面几种数据类型:
1. std::variant
std::variant是类型安全的联合体,是一个加强版的 union,variant支持更加复杂的数据类型,例如map,string等等
- 与C语言中传统的 union 类型相同的是,variant 也是联合(union)类型。即 variant 可以存放多种类型的数据,但任何时刻最多只能存放其中一种类型的数据。
- 与C语言中传统的 union 类型所不同的是,variant 是可辨识的类型安全的联合(union)类型。即 variant 无须借助外力只需要通过查询自身就可辨别实际所存放数据的类型。
v = variant<int, double, std::string>
,则 v 是一个可存放 int, double, std::string 这三种类型数据的变体类型的对象。
-
v.index()
返回变体类型 v 实际所存放数据的类型的下标。变体中第1种类型下标为0,第2种类型下标为1,以此类推。
-
std::holds_alternative<T>(v)
可查询变体类型 v 是否存放了 T 类型的数据。 -
std::get<I>(v)
如果变体类型 v 存放的数据类型下标为 I,那么返回所存放的数据,否则报错。 -
std::get_if<I>(&v)
如果变体类型 v 存放的数据类型下标为 I,那么返回所存放数据的指针,否则返回空指针。 -
std::emplace<T>()
:修改 -
std::get<T>(v)
如果变体类型 v 存放的数据类型为 T,那么返回所存放的数据,否则报错。std::get_if<T>(&v)
如果变体类型 v 存放的数据类型为 T,那么返回所存放数据的指针,否则返回空指针。
2. std::optional
std::optional表示一个可能存在的值。 当我们通过函数创建一个对象时,通常使用通过函数返回错误码,而通过出参返回对象本身。 如果通过optional返回创建的实例,就会变得更加直观,
std::optional 提供了下面几个方法:
has_value() // 检查对象是否有值
value() // 返回对象的值,值不存在时则抛出 std::bad_optional_access 异常
value_or() // 值存在时返回值,不存在时返回默认值
3. std::any
一个类型安全的可以保存任何值的容器
int main() {
//构造:
std::any s0, s1 = "Bob", s2 = string("Tom"), s3 = 3.14;
assert(s0.type() == typeid(void));
assert(s1.type() == typeid(char const*));
assert(s2.type() == typeid(string));
assert(s3.type() == typeid(double));
//创建:
auto s = make_any<A>("Jim",10); assert(s.has_value() == true);
any_cast<A>(s).print(); assert(any_cast<A>(s).age == 10);
//修改:
s.emplace<A>("Bob",20);
any_cast<A>(s).print(); assert(any_cast<A>(s).age == 20);
//清除:
s.reset(); assert(s.has_value() == false);
s = s3;
assert(any_cast<double>(s) == 3.14);
assert(*any_cast<double>(&s) == 3.14);
assert(*s._Cast<double>() == 3.14);
try {
s.reset();
double y=any_cast<double>(s); //不存在值异常
}
catch (const std::bad_any_cast& e) {
cout << "err=" << e.what() << '\n';}// err=bad any_cast
vector<any> vec{ true, 2021,string("beijing"), 3.14,A{"Tom",20} };
s.swap(s3); assert(any_cast<double>(s) == 3.14);
}
4. std::string_view
- string_view我最早使用的是boost版本的,c++17中的string_view 和 boost类似。 string_view可以理解成原始字符串一个只读引用。
- string_view 本身没有申请额外的内存来存储原始字符串的data, 仅仅保存了原始字符串地址和长度等信息。 在很多情况下,我们只是临时处理字符串,本不需要对原始字符串的一份拷贝。
- 使用string_view可以减少不必要的内存拷贝,可以提高程序性能。相比使用字符串指针,string_view做了更好的封装。
- 需要注意的是,string_view 由于没有原始字符串的所有权,使用string_view 一定要注意原始字符串的生命周期。 当原始的字符串已经销毁,则不能再调用string_view。
5.filesystem
头文件及命名空间
#include<filesystem>
using namespace std::filesystem;
常用类:
path 类:说白了该类只是对字符串(路径)进行一些处理,这也是文件系统的基石。
directory_entry 类:功如其名,文件入口,这个类才真正接触文件。
directory_iterator 类:获取文件系统目录中文件的迭代器容器,其元素为 directory_entry对象(可用于遍历目录)
file_status 类:用于获取和修改文件(或目录)的属性(需要了解C++11的强枚举类型(即枚举类))
使用方法
\1. 需要有一个path对象为基础,如果需要修改路径,可以调用其成员函数进行修改(注意其实只是处理字符串)。
2.需要获取文件信息需要通过path构造directory_entry,但需要path一定存在才能调用构造,所以需要实现调用exists(path .)函数确保目录存在才能构造directory_entry(注意文件入口中的exists无法判断)。
3.若需遍历,则可以使用 directory_iterator,进行遍历
演示如下:
#include <iostream>
#include<filesystem>
using namespace std;
using namespace std::filesystem;
int main(){
path str("C:\\Windows");
if (!exists(str)) //必须先检测目录是否存在才能使用文件入口.
return 1;
directory_entry entry(str); //文件入口
if (entry.status().type() == file_type::directory) //这里用了C++11的强枚举类型
cout << "该路径是一个目录" << endl;
directory_iterator list(str); //文件入口容器
for (auto& it:list)
cout << it.path().filename()<< endl; //通过文件入口(it)获取path对象,再得到path对象的文件名,将之输出
system("pause");
return 0;
}
常用库函数
void copy(const path& from, const path& to) :目录复制
path absolute(const path& pval, const path& base = current_path()) :获取相对于base的绝对路径
bool create_directory(const path& pval) :当目录不存在时创建目录
bool create_directories(const path& pval) :形如/a/b/c这样的,如果都不存在,创建目录结构
bool exists(const path& pval) :用于判断path是否存在
uintmax_t file_size(const path& pval) :返回目录的大小
file_time_type last_write_time(const path& pval) :返回目录最后修改日期的file_time_type对象
bool remove(const path& pval) :删除目录
uintmax_t remove_all(const path& pval) :递归删除目录下所有文件,返回被成功删除的文件个数
void rename(const path& from, const path& to) :移动文件或者重命名
6.并行STL