C++17 新特性
C++17 新特性
ISO 委员会于 2017 年 12 月接受并发布了 C++17 标准。
结构化绑定
结构化绑定允许用一个对象的成员或数组的元素,去初始化多个变量。
struct MyStruct
{
int i = 0;
std::string s;
};
MyStruct ms;
int main()
{
// 这里u和v的声明方式被称为结构化绑定
auto [u, v] = ms;// u对应i,v对应s
return 0;
}
结构化绑定还可以改变对象的值,使用引用即可:
// 通过结构化绑定改变对象的值
int main()
{
std::pair a(1, 2.3f);
auto& [i, f] = a;
i = 2;
cout << a.first << endl; // 2
}
注意结构化绑定不能应用于constexpr:
constexpr auto[x, y] = std::pair(1, 2.3f); // compile error, C++20可以
结构化绑定的两点优势:
- 无需再用成员运算符间接访问,而是可以直接访问成员,简化代码。
- 可以将值绑定到一个能体现语义的变量名上,增强代码的可读性。
带初始化的 if 和 switch 语句
C++17 中,if 和 switch 语句允许我们在条件表达式里声明一个初始化语句。
带初始化的 if 语句
if (status s = check(); s == status::success)
{
return s;
}
else
{
std::cout << "Error happend: " << s;
}
if语句中初始化了一个变量s,这个变量在整个if语句中都是可访问的。
如果初始化的是一个类,那么析构函数会在整个if语句结束时调用。
带初始化的 switch 语句
可以在 switch 语句的条件表达式之前,声明一个初始化语句来决定控制流。
enum class Color {
RED,
GREEN,
BLUE
}
......
switch(Color color = GetSelectedColor(); color) {
case RED:
std::cout << "color=" << static_cast<int>(color);
break;
case GREEN:
break;
case BLUE:
break;
default:
break;
}
初始化的color可以在整个switch语句中使用。
内联变量
C++17 之前,在类中定义的非 const 静态变量,需要在类的外面进行初始化。
C++17 引入了内联变量的概念,可以直接在类中定义并初始化非 const 静态变量:
// header file
struct A {
static const int value;
};
inline int const A::value = 10;
// ==========或者========
struct A {
inline static const int value = 10;
}
内联变量也可以用于修饰全局变量。
C++17之前,我们定义全局变量, 总需要将变量定义在cpp文件中,然后在通过extern关键字来告诉编译器这个变量已经在其他地方定义过了。如果在头文件中定义,并且该头文件被 include 到多个 cpp 文件,那么编译器会报重复定义错误。
C++17 支持直接在头文件里通过inline来定义全局变量,即使该头文件被多次include,也不会报重复定义的错误。
聚合体扩展
聚合体指数组或者 C 风格的简单类。C 风格的简单的类要求没有用户定义的构造函数、没有私有或保护的非静态数据成员、没有虚函数。另外,在 C++17 之前,还要求没有基类。
C++17 之前就有一种聚合体专有的始化方法,叫做聚合体初始化。
这是从 C 语言引入的初始化方式,是用大括号括起来的一组值来初始化类:
struct Data
{
std::string name;
double value;
};
Data x = {"test1", 6.778};
// 自从 C++11起,可以忽略等号
Data x{"test1", 6.778};
C++17 对聚合体的概念进行了扩展,聚合体可以拥有基类了。
也就是说像下面这样的派生类,也可以使用聚合体初始化:
struct Data
{
std::string name;
double value;
};
struct MoreData : Data
{
bool done;
}
// {"test1", 6.778} 用来初始化基类
MoreData y1{{"test1", 6.778}, false};
// 如果给出了基类初始化需要的所有值,可以省略内层的大括号
MoreData y2{"test1", 6.778, false};
派生类使用聚合体初始化时,使用一个子聚合体初始化来初始化基类的成员。
lambda 表达式扩展
C++17 扩展了 lambda 表达式的应用场景:
- 在 constexpr(即常量表达式)中使用,也就是在编译期间使用
- 在需要this指针的拷贝时使用
constexpr lambda
自从 C++17 起,lambda 表达式会尽可能的被隐式声明为 constexpr。
这意味着,在 lambda 表达式内部,只使用有效的编译期上下文(例如,只有字面量,没有静态变量,没有虚函数,没有 try/catch,没有 new/delete),就可以被用于编译期。
例如,可以使用一个 lambda 表达式计算参数的平方,并将计算结果用作 std::array<> 的大小:
// 自从 C++17 起被隐式声明为 constexpr
auto squared = [](auto val)
{
return val*val;
};
// 自从 C++17 起 OK,等同于 std::array<int, 25>
std::array<int, squared(5)> a;
使用 constexpr 中不允许的特性将会使 lambda 失去成为 constexpr 的能力,不过仍然可以在运行时上下文中使用 lambda:
auto squared2 = [](auto val)
{
static int calls = 0; // OK, 但会使该 lambda 不能成为 constexpr
...
return val*val;
};
std::array<int, squared2(5)> a; // ERROR:在编译期上下文中使用了静态变量
std::cout << squared2(5); // OK
为了确定一个 lambda 是否能用于编译期,可以将它主动声明为 constexpr:
auto squared3 = [](auto val) constexpr
{
return val*val;
};
如果在主动声明了 constexpr 的 lambda 内,使用了编译期上下文中不允许出现的特性,将会导致编译错误:
auto squared4 = [](auto val) constexpr
{
static int calls = 0; // ERROR: 在编译期上下文中使用了静态变量
...
return val*val;
};
在 lambda 表达式用 *this 捕获对象副本
在 C++17 之前,可以通过值或引用捕获 this:
class C
{
private:
std::string name;
public:
void foo()
{
auto l1 = [this] {std::cout << name;}; // OK
auto l2 = [=] {std::cout << name;}; // OK
auto l3 = [&] {std::cout << name;}; // OK
}
};
问题是即使是用拷贝的方式捕获 this,实质上获得的也是引用(因为只会拷贝 this 指针的值)。
当 lambda 的生命周期,比该对象的生命周期更长的时候,调用这样的 lambda 就可能导致问题。
自从 C++17 起,可以通过 *this 显式地捕获所指对象的拷贝:
class C
{
private:
std::string name;
public:
void foo()
{
// 捕获的是 this 所指对象的拷贝
auto l1 = [*this] {std::cout << name';};
}
};
嵌套命名空间
以前:
namespace A
{
namespace B
{
namespace C
{
...
}
}
}
C++ 17:
namespace A::B::C
{
...
}
UTF-8 字符字面量
自从 C++11 起,C++ 就已经支持以 u8 为前缀的 UTF8 字符串字面量。
但是,这个前缀不能用于字符字面量。
C++17 修复了这个问题,现在可以这么写:
char c = u8'6'; // UTF-8 编码的字符 '6'
如果字符不适合u8ASCII 范围,编译器将报告错误。
总结一下,字符和字符串字面量现在接受以下前缀:
- u8:用于单字节 USASCII 和 UTF8 编码
- u:用于两字节的 UTF16 编码
- U:用于四字节的UTF32 编码
- L:用于没有指定编码,可能是两个或者四个字节的宽字符集
单参数 static_assert
自从 C++17 起,static_assert()消息参数变为可选的了。
示例:
#include <type_traits>
template<typename t>
class foo
{
static_assert(std::is_default_constructible<T>::value, "class foo: elements must be default-constructible"); // 自从 C++11 起 OK
static_assert(std::is_default_constructible_v<T>); // 自从 C++17 起 OK
...
};
预处理条件 __has_include
C++17 增加了__has_include预处理指令,检查某个头文件是否存在。注意并不是检查是否已经被include过了。
示例:
#if __has_include(<Windows.h>)
#define WINDOWS_FLAG 1
#else
#define WINDOWS_FLAG 0
#endif
__has_include是一个纯粹的预处理指令。所以不能在运行时使用它:
if (__has_include("xxx.h")) // ERROR
{
...
}
新属性
C++17 之前已有的属性:
属性 | 出现版本 | 含义 |
---|---|---|
[[noreturn]] | C++11 | 函数不会返回 |
[[deprecated(“reason”)]] | C++11 | 函数已经废弃,并给出提示 |
[[carries_dependency]] | C++11 | 让编译期跳过不必要的内存栅栏指令 |
[[deprecated]] | C++14 | 函数已经废弃 |
C++17 新增了三个属性。
[[fallthrough]] 属性
[[fallthrough]]可以避免编译器在switch语句中,当某一个标签缺少break语句时发出警告。
示例:
void foo(int error)
{
switch (error)
{
case 1:
[[fallthrough]];
case 2:
std::cout << "Error happened";
break;
default:
std::cout << "OK";
break;
}
}
[[fallthrough]] 必须被用作单独的语句,还要有分号结尾。另外,在switch语句的最后一个分支不能使用。
[[nodiscard]] 属性
[[nodiscard]]可以鼓励编译器在某个函数的返回值未被使用时给出警告。
示例:
[[nodiscard]] char* foo()
{
char* p = new char[100];
...
return p;
}
foo(); // 编译器发出警告
[[maybe_unused]] 属性
[[maybe_unused]]可以避免编译器在某个变量未被使用时发出警告。
例如,可以用来定义一个可能不会使用的函数参数:
void foo(int val, [[maybe_unused]] std::string msg)
{
#ifdef DEBUG
log(msg);
#endif
...
}
还可以定义一个可能不会使用的成员:
class MyStruct
{
char c;
int i;
[[maybe_unused]] char makeLargerSize[100];
...
};
[[maybe_unused]] 可以应用于以下场景:类的声明、使用typedef或者using定义的类型、一个变量、一个非静态数据成员、一个函数、一个枚举类型、一个枚举值。
类模板参数推导
在 C++17 之前,必须明确指出类模板的所有参数。例如,不可以省略下面的double:
std::complex<double> c{5.1, 3.3};
自从 C++17 起,不用必须指明类模板参数了。
通过使用类模板参数推导,只要编译器能根据初始值推导出所有模板参数,那么就可以不指明参数。
例如:
std::complex c{5.1, 3.3}; // OK,推导出 std::complex<double>
std::mutex mx;
std::lock_guard lg{mx}; // OK,推 导 出std::lock_guard<std::mutex>
std::vector v1{1, 2, 3}; // OK,推导出 std::vector<int>
std::vector v2{"hello", "world"}; // OK,推导出 std::vector<const char*>
if constexpr
编译器在编译期决定使用if语句的哪部分,未使用的部分不会生成代码。但是语法检查还是会进行的。
示例:
template <typename T>
auto get_value(T t)
{
if constexpr (std::is_pointer_v<T>)
return *t;
else
return t;
}
折叠表达式
折叠表达式可以计算对模板参数包中的所有参数应用一个二元运算符的结果。
例如,下面的函数将会返回所有参数的总和:
template<typename... T>
auto sum(T... args)
{
return (... + args); // ((arg1 + arg2) + arg3)...
}
新的标准库组件
C++17 新加了很多有用的标准库组件。
std::optional<>
我们有时候会有这样的需求,让函数返回一个对象,但错误情况下,怎么返回个空呢?
有一种办法是返回对象指针,失败情况下可以返回std::nullptr。
但是这就涉及到了内存管理,虽然使用智能指针可以避免手动进行内存管理。
但 C++17 有了更方便的办法:std::optional<>。
示例:
std::optional<int> asInt(const std::string &s)
{
try {
return std::stoi(s);
} catch(...) {
return std::nullopt; // 返回一个空的 std::optional
}
}
std::string s{"123"};
std::optional<int> oi = asInt(s);
// 判断是否有值
if (oi)
{
cout << *oi << endl;
}
else
{
cout << "error" << endl;
}
std::variant<>
C++17 新增了std::variant组件,实现类似union(联合体),但却比union更方便。
比如,union里面不能有std::string这种类型,但std::variant却可以,还可以支持更多复杂类型,如std::map等。
示例:
#include <variant>
#include <iostream>
int main()
{
std::variant<int, std::string> var{"hi"}; // 可以持有 int, std::string 两种类型的值,初始化为 std::string
std::cout << var.index(); // 打印出 1
var = 42; // 现在持有 int 类型
std::cout << var.index(); // 打印出 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();
...
}
// get_if
// get_if<T> 返回指向类型为 T 的值的指针或 nullptr
// get_if<Index> 返回指向索引为 Index 的值的指针或 nullptr
std::string* s = std::get_if<std::string>(&var); // s == nullptr
}
std::any
std::any可以存储任何类型的单个值。
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))
{
std::string s = std::any_cast<std::string>(a);
useString(s);
}
else if (a.type() == typeid(int))
{
useInt(std::any_cast<int>(a));
}
为了访问内部的值,必须使用std::any_cast<>将它转换为真正的类型:
auto s = std::any_cast<std::string>(a);
如果转换失败(可能是因为对象为空,或者类型不匹配),会抛出一个std::bad_any_cast异常:
try {
auto s = std::any_cast<std::string>(a);
...
} catch (std::bad_any_cast& e) {
std::cerr << "EXCEPTION: " << e.what();
}
也可以对std::any对象的地址进行转换,如果转换失败,将返回std::nullptr。
auto p = std::any_cast<std::string>(&a);
std::shared_mutex
这个就是读写锁。
std::string_view
std::string_view可以获取一个字符串的视图,字符串视图并不真正的创建或者拷贝字符串,而只是拥有一个字符串的查看功能。
std::string_view比std::string的性能高很多,std::string_view只是记录了对应的字符串的指针和偏移位置。
std::string_view对指向的内容是只读的,当我们在只是查看字符串内容的时候,使用std::string_view来代替std::string。
示例:
void foo(std::string_view strv)
{
cout << strv << endl;
}
int main()
{
std::string str = "Hello World";
std::string_view strv(str.c_str(), str.size());
foo(strv);// Hello World
return 0;
}
文件系统
C++17 终于将文件系统纳入标准中,提供了关于文件系统的很多功能,基本上应有尽有,这里简单举几个例子:
namespace fs = std::filesystem;
// 创建目录
fs::create_directory(fs::path("tmp/test"));
// 创建目录树
fs::create_directories(fs::path("tmp/test/subdir"));
// 拷贝文件
fs::copy_file(src, dst, fs::copy_options::skip_existing);
// 判断文件是否存在
bool is_exist = fs::exists(filename);
// path 代表一个文件系统路径
std::filesystem::path p{"c:\\1.txt"};
// 路径 p 是普通文件吗?
if (is_regular_file(p))
{
std::cout << file_size(p) << " bytes";
}
// 路径 p 是目录吗?
if (is_directory(p))
{
// 遍历目录
for (auto& e : std::filesystem::directory_iterator{p})
{
std::cout << " " << e.path() <<;
}
}
小结
C++17 是一个比较大的更新。
C++ 越来越方便,但是也越来越复杂。