文章目录

  • 一、C++ 标准发布历史
  • 二、基本语言特性
    • 1. 结构化绑定
      • 结构化绑定的两点优势
      • 一个使用结构化绑定来改进代码的例子
    • 2. 带初始化的`if` 和`switch` 语句
      • 2.1 带初始化的`if`语句
      • 2.2 带初始化的`switch`语句
    • 3. 内联变量
      • 内联变量也可以用于修饰全局变量
    • 4. 聚合体扩展
      • 聚合体扩展的动机
    • 5. lambda 表达式扩展
      • 5.1 constexpr lambda
      • 5.2 向 lambda 传递 this 的拷贝
    • 6. 新属性
      • 6.1 [[fallthrough]] 属性
      • 6.2 [[nodiscard]] 属性
      • 6.3 [[maybe_unused]] 属性
    • 7. 其他语言特性
      • 7.1 嵌套命名空间
      • 7.2 UTF-8 字符字面量
      • 7.3 单参数 static_assert
      • 7.4 预处理条件 __has_include
  • 三、模板特性
    • 8. 类模板参数推导
    • 9. if constexpr(编译期 if 语句)
    • 10. 折叠表达式
  • 四、新的标准库组件
    • 11. std::optional<>
    • 12. std::variant<>
    • 13. std::any
    • 14. std::shared_mutex
    • 15. std::string_view
    • 16. 文件系统
  • 五、总结

一、C++ 标准发布历史

2017年12月,正式发布了 C++17。

二、基本语言特性

1. 结构化绑定

结构化绑定允许用一个对象的成员或数组的元素,去初始化多个变量。

struct MyStruct {int i = 0;std::string s;
};MyStruct ms;

声明用两个变量,直接绑定两个成员:

auto [u,v] = ms;

这里uv的声明方式被称为结构化绑定

结构化绑定对于返回结构体或数组的函数尤其有用。

例如,有一个返回一个结构体的函数:

MyStruct getStruct() {return MyStruct{42, "hello"};
}

可以直接把返回值的两个数据成员赋值给两个局部变量:

auto [id,val] = getStruct(); //`id` 和`val` 分别对应返回结构体的`i` 和`s` 成员

在这里,idval 分别绑定到返回的结构体中名为is 的成员。

它们的类型分别是intstd::string

结构化绑定的两点优势

  • 一是无需再用成员运算符间接访问,而是可以直接访问成员,简化代码
  • 二是可以将值绑定到一个能体现语义的变量名上,增强代码的可读性

一个使用结构化绑定来改进代码的例子

在没有结构化绑定的情况下,为了迭代一个std::map<> 的元素会像下边这样写:

for (const auto& element : mymap) {std::cout << element.first << ": " << element.second;
}

通过使用结构化绑定,可以这么写:

for (const auto& [key,val] : mymap) {std::cout << key << ": " << val;
}

通过这种方式,我们可以直接用简单的变量名来访问每个元素的键和值,让代码的可读性更强。

2. 带初始化的ifswitch 语句

C++17 中,ifswitch 语句允许我们在条件表达式里声明一个初始化语句。

2.1 带初始化的if语句

现在可以这样写if语句:

if (status s = check(); s == status::success) {return s;
}

if语句中初始化了一个变量s,这个变量在整个if语句中都是可访问的。

也可以在else语句中访问,例如:

if (status s = check(); s == status::success) {return s;
} else {std::cout << "Error happend: " << s;
}
// 到此处 s 不再有效

如果初始化的是一个类,那么析构函数会在整个if语句结束时调用。

2.2 带初始化的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语句中使用。

3. 内联变量

C++17 之前,在类中定义的非 const 静态变量,需要在类的外面进行初始化,如:

class MyClass {static std::string msg;...
};
std::string MyClass::msg{"OK"};

C++17 引入了内联变量的概念,可以直接在类中定义并初始化非 const 静态变量。

class MyClass {inline static std::string msg{"OK"}; // 自从 C++17 起 OK...
};

内联变量也可以用于修饰全局变量

C++17 之前,全局变量只能在 cpp 文件中定义,然后在头文件中声明。

如果在头文件中定义,并且该头文件被 include 到多个 cpp 文件,那么编译器会报重复定义错误。

C++17 可以直接在头文件里通过inline来定义全局变量,即使该头文件被多次include,也不会报重复定义的错误。

// .h 文件
#include<xxx.h>
inline MyClass myGlobalObj; // 即使被多个 cpp 文件包含也 OK

4. 聚合体扩展

C++17 之前就有一种聚合体专有的始化方法,叫做聚合体初始化

这是从 C 语言引入的初始化方式,是用大括号括起来的一组值来初始化类:

struct Data {std::string name;double value;
};
Data x = {"test1", 6.778};

自从 C++11起,可以忽略等号:

Data x{"test1", 6.778};

聚合体指数组或者C 风格的简单类。C 风格的简单的类要求没有用户定义的构造函数、没有私有或保护的非静态数据成员、没有虚函数。另外,在 C++17 之前,还要求没有基类。

C++17 对聚合体的概念进行了扩展,聚合体可以拥有基类了。

也就是说像下面这样的派生类,也可以使用聚合体初始化:

struct MoreData : Data {bool done;
}
MoreData y{{"test1", 6.778}, false}; // {"test1", 6.778} 用来初始化基类

派生类使用聚合体初始化时,使用一个子聚合体初始化来初始化基类的成员。

聚合体扩展的动机

如果没有这个特性,我们要像下面这样定义构造函数来进行初始化:

struct Cpp14Data : Data {bool done;Cpp14Data (const std::string& s, double d, bool b) : Data{s, d}, done{b} {}
};
Cpp14Data y{"test1", 6.778, false};

现在我们不需要定义构造函数,可以直接使用聚合体初始化:

MoreData x{{"test1", 6.778}, false};

如果给出了基类初始化需要的所有值,可以省略内层的大括号:

MoreData y{"test1", 6.778, false};

5. lambda 表达式扩展

C++17 扩展了 lambda 表达式的应用场景:

  • 在 constexpr(即常量表达式)中使用,也就是在编译期间使用
  • 在需要this指针的拷贝时使用

5.1 constexpr lambda

自从 C++17 起,lambda 表达式会尽可能的被隐式声明为 constexpr。

这意味着,在 lambda 表达式内部,只使用有效的编译期上下文(例如,只有字面量,没有静态变量,没有虚函数,没有 try/catch,没有 new/delete),就可以被用于编译期。

例如,可以使用一个 lambda 表达式计算参数的平方,并将计算结果用作 std::array<> 的大小:

auto squared = [](auto val) {  // 自从 C++17 起被隐式声明为 constexprreturn val*val;
};
std::array<int, squared(5)> a; // 自从 C++17 起 OK,等同于 std::array<int, 25>

使用 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;
};

5.2 向 lambda 传递 this 的拷贝

在 C++17 之前,可以通过值或引用捕获 this:

class C {private:std::string name;
public:...void foo() {auto l1 = [this] {std::cout << name;}; // OKauto l2 = [=] {std::cout << name;};    // OKauto l3 = [&] {std::cout << name;};    // OK...}
};

问题是即使是用拷贝的方式捕获 this,实质上获得的也是引用(因为只会拷贝 this 指针的值)。

当 lambda 的生命周期,比该对象的生命周期更长的时候,调用这样的 lambda 就可能导致问题。

自从 C++17 起,可以通过 *this 显式地捕获所指对象的拷贝:

class C {private:std::string name;
public:...void foo() {auto l1 = [*this] {std::cout << name';}; // 捕获的是 this 所指对象的拷贝...}
};

6. 新属性

C++17 之前已有的属性:

属性 出现版本 含义
[[noreturn]] (since C++11) 函数不会返回
[[deprecated("reason")]] (since C++11) 函数已经废弃,并给出提示
[[carries_dependency]] (since C++11) 让编译期跳过不必要的内存栅栏指令
[[deprecated]] (since C++14) 函数已经废弃

C++17 新增了三个属性。

6.1 [[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语句的最后一个分支不能使用。

6.2 [[nodiscard]] 属性

[[nodiscard]]              (since C++17)
[[nodiscard("reason")]]    (since C++20)

[[nodiscard]]可以鼓励编译器在某个函数的返回值未被使用时给出警告。

[[nodiscard]] char* foo() {char* p = new char[100];...return p;
}
...foo(); // 编译器发出警告

6.3 [[maybe_unused]] 属性

[[maybe_unused]]可以避免编译器在某个变量未被使用时发出警告。

例如,可以用来定义一个可能不会使用的函数参数:

void foo(int val, [[maybe_unused]] std::string msg)
{#ifdef DEBUGlog(msg);
#endif...
}

还可以定义一个可能不会使用的成员:

class MyStruct {char c;int i;[[maybe_unused]] char makeLargerSize[100];...
};

[[maybe_unused]]可以应用于以下场景:类的声明、使用typedef或者using定义的类型、一个变量、一个非静态数据成员、一个函数、一个枚举类型、一个枚举值。

7. 其他语言特性

7.1 嵌套命名空间

以前:

namespace A {namespace B {namespace C {...}}
}

C++ 17:

namespace A::B::C {...
}

这个特性是 2003 年提出的…

7.2 UTF-8 字符字面量

自从 C++11 起,C++ 就已经支持以 u8 为前缀的 UTF­8 字符串字面量。

但是,这个前缀不能用于字符字面量。

C++17 修复了这个问题,现在可以这么写:

char c = u8'6'; // UTF-8 编码的字符 '6'

总结一下,字符和字符串字面量现在接受以下前缀:

  • u8:用于单字节 US­ASCII 和 UTF­8 编码
  • u:用于两字节的 UTF­16 编码
  • U:用于四字节的UTF­32 编码
  • L:用于没有指定编码,可能是两个或者四个字节的宽字符集

7.3 单参数 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 起 OKstatic_assert(std::is_default_constructible_v<T>); // 自从 C++17 起 OK...
};

7.4 预处理条件 __has_include

C++17 增加了__has_include预处理指令,检查某个头文件是否存在。注意并不是检查是否已经被include过了。

__has_include ("文件名")
__has_include (<文件名>)
#if __has_include(<Windows.h>)#define WINDOWS_FLAG 1
#else#define WINDOWS_FLAG 0
#endif

__has_include是一个纯粹的预处理指令。所以不能在运行时使用它:

if (__has_include("xxx.h")) { // ERROR
}

三、模板特性

8. 类模板参数推导

在 C++17 之前,必须明确指出类模板的所有参数。例如,不可以省略下面的double

std::complex<double> c{5.1, 3.3};

也不可以省略下面的 std::mutex:

std::mutex mx;
std::lock_guard<std::mutex> lg(mx);

自从 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*>

9. if constexpr(编译期 if 语句)

template <typename T>
auto get_value(T t) {if constexpr (std::is_pointer_v<T>)return *t;elsereturn t;
}

编译器在编译期决定使用if语句的哪部分,未使用的部分不会生成代码。但是语法检查还是会进行的。

10. 折叠表达式

折叠表达式可以计算对模板参数包中的所有参数应用一个二元运算符的结果。

例如,下面的函数将会返回所有参数的总和:

template<typename... T>auto sum(T... args) {return (... + args); // ((arg1 + arg2) + arg3)...
}

如下调用:

sum(47, 11, val, -1);

会把模板实例化为:

return 47 + 11 + val + -1;

如下调用:

sum(std::string("hello"), "world", "!");

会把模板实例化为:

return std::string("hello") + "world" + "!";

四、新的标准库组件

C++17 新加了很多有用的标准库组件。

11. std::optional<>

我们有时候会有这样的需求,让函数返回一个对象,如下,错误情况下,怎么返回个空呢?

struct A {...
};A foo() {bool ok = bar();if (ok) {return A();} else {// 怎么返回个空呢?}
}

有一种办法是返回对象指针,失败情况下可以返回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;
}

12. 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::stringstd::cout << var.index();                 // 打印出 1var = 42;                                 // 现在持有 int 类型std::cout << var.index();                 // 打印出 0try {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 的值的指针或 nullptrstd::string* s = std::get_if<std::string>(&var); // s == nullptr
}

13. 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);
if (p != nullptr) {...
}

14. std::shared_mutex

这个就是读写锁,就不多讲了。

15. std::string_view

void foo(std::string_view strv)
{cout << strv << endl;
}int main(void)
{std::string str = "Hello World";std::string_view strv(str.c_str(), str.size());foo(strv);return 0;
}


std::string_view可以获取一个字符串的视图,字符串视图并不真正的创建或者拷贝字符串,而只是拥有一个字符串的查看功能。

std::string_viewstd::string的性能高很多,std::string_view只是记录了对应的字符串的指针和偏移位置。

std::string_view对指向的内容是只读的,当我们在只是查看字符串内容的时候,使用std::string_view来代替std::string

16. 文件系统

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() <<;}
}

五、总结

  1. 本次介绍了 C++17 中大部分新特性,个别特性没有介绍。

    如并行 STL 算法,这个特性为几乎所有标准库函数加上一个执行策略参数,可以让使用者选择并行还是串行。

  2. C++17 是一个比较大的更新。

  3. C++ 越来越方便,但是也越来越复杂。

C++17 新特性介绍相关推荐

  1. JDK 9-17 新特性介绍

    Java新特性介绍 Java 8是Oracle 公司于 2014 年 3 月 18 日发布的,距离今天已经过了近十年的时间了,但是由于Java 8的稳定和生态完善(目前仍是LTS长期维护版本),依然有 ...

  2. 2022年最细Java 17新特性,是真的猛,被征服了!

    SpringBoot 正式支持Java 17,Kafka3.0弃用Java8 Spring Boot 2.5.5是Spring Boot 第一个支持Java 17的版本.现在你已经可以从Spring ...

  3. k8s v1.17 新特性预告: 拓扑感知服务路由

    大家好,我是 roc,来自腾讯云容器服务(TKE)团队,今天给大家介绍下我参与开发的一个 k8s v1.17 新特性: 拓扑感知服务路由. 01 名词解释 拓扑域: 表示在集群中的某一类 " ...

  4. Java 17新特性,快到起飞?惊呆了!

    都说Java 8 是YYDS,那你注意到 Java 17 也是长期支持版本吗?目前按计划 JDK 19 将于今年 9 月发布 SpringBoot 正式支持Java 17,Kafka3.0弃用Java ...

  5. Java 16 新特性介绍

    本文要点 Java 16 和即将发布的 Java 17 引入了大量特性和语言增强,有助于提高开发人员的生产力和应用程序性能 Java 16 Stream API 为常用的终端操作提供了很多新方法,有助 ...

  6. C++11\14\17\20 特性介绍

    C++11 新特性 #01 auto 与 decltype auto: 对于变量,指定要从其初始化器⾃动推导出其类型.⽰例: auto a = 10; // 自动推导 a 为 int auto b = ...

  7. C++11/14/17 新特性总结

    C++11/14/17 新特性总结 initializer_list std::vector<int> vctInts({92, 12, 39, 46, 92, 84, -1, 0, -2 ...

  8. Java SE 笔记(扩展篇)JDK 9-17 新特性介绍

    Java新特性介绍 **注意:**推荐完成此路线所有前置内容后,再来学习本篇. 经过前面的学习,我们基本已经了解了Java 8及之前的所有语法,不过,Java 8是Oracle 公司于 2014 年 ...

  9. Portlet 2.0 新特性介绍(全)

    第一部分 Portlet 2.0 新特性介绍 ====================================================================== 关于本系列 ...

最新文章

  1. python单行箭头_python – 如何在matplotlib的曲线末端放置一个箭头?
  2. oracle数据抽取脚本,oracle的一些信息抽取脚本.sql
  3. VTK:可视化算法之StreamlinesWithLineWidget
  4. 如何实现Spark on Kubernetes?
  5. 50 Python - 装饰器 类定义装饰器
  6. 2021抖音电商宠物经济洞察报告
  7. JSON数据格式转换(StringBuffer拼接json串)大全及用法
  8. 44. Factor parameter-independent code out of templates.
  9. UOS浏览器不支持NPAPI插件,怎么办?
  10. Android性能调优工具TraceView
  11. 电脑无损分区大小调整
  12. SQL语句练习题(壹)
  13. p6spy mysql8_P6Spy监控SQL语句及性能测试
  14. opengl 加载obj模型
  15. Multisim 编码器 译码器 74LS138 74LS148
  16. Vue前端验证一个text只能输入手机号或邮箱
  17. 信息安全风险评估_一般找谁做?
  18. [Win32]路径操作API函数
  19. CentOS7 安装 RabbitMQ 3.6(方法适用于安装任意版本 RabbitMQ)
  20. 腾讯云数据库 TDSQL-之初体验

热门文章

  1. 乐字节十个项目课,这应该是市面上最值得推荐的 Java 练手项目!!!
  2. 计算机右击管理在哪,电脑权限管理在哪里
  3. 计算机二级第二课堂学记录表,第二课堂活动记录.doc
  4. 雨课堂卷子提前看_雨课堂怎么新建试卷 制作试题的方法
  5. TLA+ 术语解释及中文翻译
  6. 技术文档中的各种架构图该怎么画? by彭文华
  7. python 基于win32clipboard、wxpython下的剪切板工具
  8. Python学习笔记(一) Python基础
  9. 自建CDN技术选型: squid varnish nginx
  10. DDR5 On Die ECC