|

本文详细介绍了C++17标准中引入的所有重要特性。
C++17 包含以下新的语言特性:
类模板的模板参数推导使用 auto 声明非类型模板参数折叠表达式从花括号初始化列表推导 auto 的新规则constexpr lambda按值捕获 this内联变量嵌套命名空间结构化绑定带初始化器的选择语句constexpr ifUTF-8 字符字面量枚举的直接列表初始化[fallthrough]、[nodiscard]、[maybe_unused] 属性__has_include类模板参数推导C++17 包含以下新的库特性:
std::variantstd::optionalstd::anystd::string_viewstd::invokestd::applystd::filesystemstd::byte映射和集合的拼接并行算法std::samplestd::clampstd::reduce前缀和算法最大公约数和最小公倍数std::not_fn字符串与数字的相互转换chrono 持续时间和时间点的舍入函数C++17 语言特性类模板的模板参数推导类模板的模板参数推导类似于函数的模板参数推导,但现在也包括类的构造函数。
template typename T = float>
struct MyContainer {
T val;
MyContainer() : val{} {}
MyContainer(T val) : val{val} {}
// ...
};
MyContainer c1 {1}; // OK,推导为 MyContainer
MyContainer c2; // OK,推导为 MyContainer
使用 auto 声明非类型模板参数遵循 auto 的推导规则,同时尊重非类型模板参数允许的类型列表,模板参数可以从其参数的类型中推导出来:
template auto... seq>
struct my_integer_sequence {
// 实现部分 ...
};
// 显式传递类型 `int` 作为模板参数。
auto seq = std::integer_sequenceint, 0, 1, 2>();
// 类型被推导为 `int`。
auto seq2 = my_integer_sequence0, 1, 2>(); - 例如,不能使用 double 作为模板参数类型,这也使得使用 auto 进行推导无效。
折叠表达式折叠表达式是对模板参数包在二元运算符上的折叠操作。
形如 (... op e) 或 (e op ...) 的表达式,其中 op 是折叠运算符,e 是未展开的参数包,称为 一元折叠。形如 (e1 op ... op e2) 的表达式,其中 op 是折叠运算符,e1 或 e2 是未展开的参数包,但不能同时是,称为 二元折叠。template typename... Args>
bool logicalAnd(Args... args) {
// 二元折叠。
return (true && ... && args);
}
bool b = true;
bool& b2 = b;
logicalAnd(b, b2, true); // == true
template typename... Args>
auto sum(Args... args) {
// 一元折叠。
return (... + args);
}
sum(1.0, 2.0f, 3); // == 6.0
从花括号初始化列表推导 auto 的新规则当使用统一初始化语法时,auto 的推导规则发生了变化。以前,auto x {3}; 推导为 std::initializer_list,现在则推导为 int。
auto x1 {1, 2, 3}; // 错误:不是单个元素
auto x2 = {1, 2, 3}; // x2 是 std::initializer_list
auto x3 {3}; // x3 是 int
auto x4 {3.0}; // x4 是 double
constexpr lambda使用 constexpr 的编译时 lambda 表达式。
auto identity = [](int n) constexpr { return n; };
static_assert(identity(123) == 123);
constexpr auto add = [](int x, int y) {
auto L = [=] { return x; };
auto R = [=] { return y; };
return [=] { return L() + R(); };
};
static_assert(add(1, 2)() == 3);
constexpr int addOne(int n) {
return [n] { return n + 1; }();
}
static_assert(addOne(1) == 2);
按值捕获 this以前,lambda 表达式中的 this 只能通过引用捕获。在异步代码中使用回调时,这可能是一个问题,因为回调可能需要一个对象,而该对象可能已经超出其生命周期。*this(C++17)现在会复制当前对象,而 this(C++11)仍然通过引用捕获。
struct MyObj {
int value {123};
auto getValueCopy() {
return [*this] { return value; };
}
auto getValueRef() {
return [this] { return value; };
}
};
MyObj mo;
auto valueCopy = mo.getValueCopy();
auto valueRef = mo.getValueRef();
mo.value = 321;
valueCopy(); // 123
valueRef(); // 321
内联变量内联说明符可以应用于变量,就像应用于函数一样。声明为内联的变量具有与声明为内联的函数相同的语义。
// 使用编译器探索器的反汇编示例。
struct S { int x; };
inline S x1 = S{321}; // mov esi, dword ptr [x1]
// x1: .long 321
S x2 = S{123}; // mov eax, dword ptr [.L_ZZ4mainE2x2]
// mov dword ptr [rbp - 8], eax
// .L_ZZ4mainE2x2: .long 123
它还可以用于声明和定义一个静态成员变量,这样就不需要在源文件中初始化它了。
struct S {
S() : id{count++} {}
~S() { count--; }
int id;
static inline int count{0}; // 在类内声明并初始化 count 为 0
};
嵌套命名空间使用命名空间解析运算符来创建嵌套命名空间定义。
namespace A {
namespace B {
namespace C {
int i;
}
}
}
上面的代码可以写成这样:
namespace A::B::C {
int i;
}
结构化绑定一个用于解构初始化的提案,允许编写 auto [ x, y, z ] = expr;,其中 expr 的类型是一个类似元组的对象,其元素将绑定到变量 x、y 和 z(这些变量由该构造声明)。类似元组的对象包括 std::tuple、std::pair、std::array 和聚合结构。
using Coordinate = std::pairint, int>;
Coordinate origin() {
return Coordinate{0, 0};
}
const auto [ x, y ] = origin();
x; // == 0
y; // == 0
std::unordered_mapstd::string, int> mapping {
{"a", 1},
{"b", 2},
{"c", 3}
};
// 按引用解构。
for (const auto& [key, value] : mapping) {
// 对 key 和 value 进行操作
}
带初始化器的选择语句if 和 switch 语句的新版本,简化了常见的代码模式,并帮助用户保持作用域紧凑。
{
std::lock_guardstd::mutex> lk(mx);
if (v.empty()) v.push_back(val);
}
// vs.
if (std::lock_guardstd::mutex> lk(mx); v.empty()) {
v.push_back(val);
}
Foo gadget(args);
switch (auto s = gadget.status()) {
case OK: gadget.zip(); break;
case Bad: throw BadFoo(s.message());
}
// vs.
switch (Foo gadget(args); auto s = gadget.status()) {
case OK: gadget.zip(); break;
case Bad: throw BadFoo(s.message());
}
constexpr if根据编译时条件实例化代码。
template typename T>
constexpr bool isIntegral() {
if constexpr (std::is_integral::value) {
returntrue;
} else {
returnfalse;
}
}
static_assert(isIntegralint>() == true);
static_assert(isIntegralchar>() == true);
static_assert(isIntegraldouble>() == false);
struct S {};
static_assert(isIntegral() == false);
UTF-8 字符字面量以 u8 开头的字符字面量是类型为 char 的字符字面量。UTF-8 字符字面量的值等于其 ISO 10646 代码点值。
char x = u8'x';
枚举的直接列表初始化现在可以使用花括号语法初始化枚举。
enum byte : unsigned char {};
byte b {0}; // OK
byte c {-1}; // 错误
byte d = byte{1}; // OK
byte e = byte{256}; // 错误
[[fallthrough]]、[[nodiscard]]、[[maybe_unused]] 属性C++17 引入了三个新的属性:[[fallthrough]]、[[nodiscard]] 和 [[maybe_unused]]。
[[fallthrough]] 告诉编译器在 switch 语句中故意穿透。此属性只能在 switch 语句中使用,并且必须放在下一个 case/default 标签之前。switch (n) {
case1:
// ...
[[fallthrough]];
case2:
// ...
break;
case3:
// ...
[[fallthrough]];
default:
// ...
}
[[nodiscard]] 当函数或类具有此属性且其返回值被丢弃时发出警告。[[nodiscard]] bool do_something() {
return is_success; // 成功返回 true,失败返回 false
}
do_something(); // 警告:忽略 'bool do_something()' 的返回值,
// 该函数声明了 'nodiscard' 属性
// 仅在通过值返回 `error_info` 时发出警告。
struct [[nodiscard]] error_info {
// ...
};
error_info do_something() {
error_info ei;
// ...
return ei;
}
do_something(); // 警告:忽略返回的 'error_info' 类型的值,
// 该类型声明了 'nodiscard' 属性
[[maybe_unused]] 告诉编译器某个变量或参数可能未使用,这是有意为之。void my_callback(std::string msg, [[maybe_unused]] bool error) {
// 不关心 `msg` 是否是错误消息,只是记录它。
log(msg);
}
__has_include__has_include (operand) 运算符可以在 #if 和 #elif 表达式中使用,以检查某个头文件或源文件(operand)是否可以被包含。
一个用例是使用两个功能相同的库,如果首选的库在系统中找不到,则使用备用/实验性库。
#ifdef __has_include
# if __has_include()
# include
# define have_optional 1
# elif __has_include()
# include
# define have_optional 1
# define experimental_optional
# else
# define have_optional 0
# endif
#endif
它还可以用于包含在不同平台下名称或位置不同的头文件,而无需知道程序运行在哪个平台上。OpenGL 头文件是一个很好的例子,它在 macOS 上位于 OpenGL\ 目录,在其他平台上位于 GL\。
#ifdef __has_include
# if __has_include()
# include
# include
# elif __has_include()
# include
# include
# else
# error 未找到合适的 OpenGL 头文件。
# endif
#endif
类模板参数推导类模板参数推导(CTAD)允许编译器从构造函数参数中推导模板参数。
std::vector v{ 1, 2, 3 }; // 推导为 std::vector
std::mutex mtx;
auto lck = std::lock_guard{ mtx }; // 推导为 std::lock_guard
auto p = new std::pair{ 1.0, 2.0 }; // 推导为 std::pair*
对于用户定义的类型,可以使用 推导指南 来指导编译器如何推导模板参数(如果适用):
template typename T>
struct container {
container(T t) {}
template typename Iter>
container(Iter beg, Iter end);
};
// 推导指南
template typename Iter>
container(Iter b, Iter e) -> containertypenamestd::iterator_traits[I]::value_type>;
container a{ 7 }; // OK:推导为 container
std::vectordouble> v{ 1.0, 2.0, 3.0 };
auto b = container{ v.begin(), v.end() }; // OK:推导为 container
container c{ 5, 6 }; // 错误:std::iterator_traits::value_type 不是类型
C++17 库特性std::variant类模板 std::variant 表示一个类型安全的 union。std::variant 的实例在任何给定时间都持有一个其替代类型的值(它也可以是无值的)。
std::variantint, double> v{ 12 };
std::getint>(v); // == 12
std::get0>(v); // == 12
v = 12.0;
std::getdouble>(v); // == 12.0
std::get1>(v); // == 12.0
std::optional类模板 std::optional 管理一个可选的包含值,即一个可能存在也可能不存在的值。optional 的常见用例是函数的返回值,该函数可能会失败。
std::optionalstd::string> create(bool b) {
if (b) {
return"Godzilla";
} else {
return {};
}
}
create(false).value_or("empty"); // == "empty"
create(true).value(); // == "Godzilla"
// 返回 optional 的工厂函数可以用作 while 和 if 的条件
if (auto str = create(true)) {
// ...
}
std::any一个类型安全的容器,用于存储任何类型的单个值。
std::any x {5};
x.has_value() // == true
std::any_castint>(x) // == 5
std::any_castint&>(x) = 10;
std::any_castint>(x) // == 10
std::string_view对字符串的非拥有引用。它适用于在字符串上提供抽象(例如用于解析)。
// 普通字符串。
std::string_view cppstr {"foo"};
// 宽字符串。
std::wstring_view wcstr_v {L"baz"};
// 字符数组。
char array[3] = {'b', 'a', 'r'};
std::string_view array_v(array, std::size(array));
std::string str {" trim me"};
std::string_view v {str};
v.remove_prefix(std::min(v.find_first_not_of(" "), v.size()));
str; // == " trim me"
v; // == "trim me"
std::invoke调用一个 Callable 对象及其参数。可调用 对象的示例包括 std::function 或 lambda;可以像普通函数一样调用的对象。
template typename Callable>
class Proxy {
Callable c_;
public:
Proxy(Callable c) : c_{ std::move(c) } {}
template typename... Args>
decltype(auto) operator()(Args&&... args) {
// ...
returnstd::invoke(c_, std::forward(args)...);
}
};
constauto add = [](int x, int y) { return x + y; };
Proxy p{ add };
p(1, 2); // == 3
std::apply使用元组的参数调用一个 Callable 对象。
auto add = [](int x, int y) {
return x + y;
};
std::apply(add, std::make_tuple(1, 2)); // == 3
std::filesystem新的 std::filesystem 库提供了一种标准的方式来操作文件、目录和文件系统中的路径。
以下是一个大文件被复制到临时路径的示例,前提是存在足够的空间:
const auto bigFilePath {"bigFileToCopy"};
if (std::filesystem::exists(bigFilePath)) {
const auto bigFileSize {std::filesystem::file_size(bigFilePath)};
std::filesystem::path tmpPath {"/tmp"};
if (std::filesystem::space(tmpPath).available > bigFileSize) {
std::filesystem::create_directory(tmpPath.append("example"));
std::filesystem::copy_file(bigFilePath, tmpPath.append("newFile"));
}
}
std::byte新的 std::byte 类型提供了一种标准的方式来表示数据为字节。使用 std::byte 而不是 char 或 unsigned char 的好处是它不是字符类型,也不是算术类型;唯一可用的运算符重载是按位运算。
std::byte a {0};
std::byte b {0xFF};
int i = std::to_integerint>(b); // 0xFF
std::byte c = a & b;
int j = std::to_integerint>(c); // 0
注意,std::byte 只是一个枚举类型,而枚举的直接列表初始化成为可能,这得益于枚举的直接列表初始化。
映射和集合的拼接在没有昂贵的拷贝、移动或堆分配/释放开销的情况下,移动节点和合并容器。
从一个映射移动元素到另一个映射:
std::mapint, string> src {{1, "one"}, {2, "two"}, {3, "buckle my shoe"}};
std::mapint, string> dst {{3, "three"}};
dst.insert(src.extract(src.find(1))); // 从 `src` 到 `dst` 廉价地移除并插入 { 1, "one" }
dst.insert(src.extract(2)); // 从 `src` 到 `dst` 廉价地移除并插入 { 2, "two" }
// dst == { { 1, "one" }, { 2, "two" }, { 3, "three" } };
插入整个集合:
std::setint> src {1, 3, 5};
std::setint> dst {2, 4, 5};
dst.merge(src);
// src == { 5 }
// dst == { 1, 2, 3, 4, 5 }
插入超出容器生命周期的元素:
auto elementFactory() {
std::set s;
s.emplace(...);
return s.extract(s.begin());
}
s2.insert(elementFactory());
更改映射元素的键:
std::mapint, string> m {{1, "one"}, {2, "two"}, {3, "three"}};
auto e = m.extract(2);
e.key() = 4;
m.insert(std::move(e));
// m == { { 1, "one" }, { 3, "three" }, { 4, "two" } }
并行算法许多 STL 算法(如 copy、find 和 sort)开始支持 并行执行策略:seq、par 和 par_unseq,分别表示"顺序"、"并行"和"并行无序"。
std::vectorint> longVector;
// 使用并行执行策略查找元素
auto result1 = std::find(std::execution::par, std::begin(longVector), std::end(longVector), 2);
// 使用顺序执行策略排序元素
auto result2 = std::sort(std::execution::seq, std::begin(longVector), std::end(longVector));
std::sample从给定序列中随机抽样 n 个元素(不放回),每个元素被选中的概率相等。
const std::string ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
std::string guid;
// 从 ALLOWED_CHARS 中随机抽样 5 个字符。
std::sample(ALLOWED_CHARS.begin(), ALLOWED_CHARS.end(), std::back_inserter(guid),
5, std::mt19937{ std::random_device{}() });
std::cout // 例如:G1fW2
std::clamp将给定值限制在上下界之间。
std::clamp(42, -1, 1); // == 1
std::clamp(-42, -1, 1); // == -1
std::clamp(0, -1, 1); // == 0
// `std::clamp` 也接受自定义比较器:
std::clamp(0, -1, 1, std::less{}); // == 0
std::reduce对给定范围的元素进行折叠操作。它与 std::accumulate 概念上类似,但 std::reduce 会并行执行折叠操作。由于折叠是并行执行的,如果指定了二元运算符,则要求它是结合的和交换的。给定的二元运算符也不应修改范围内的任何元素或使任何迭代器失效。
默认的二元运算是 std::plus,初始值为 0。
const std::arrayint, 3> a{ 1, 2, 3 };
std::reduce(std::cbegin(a), std::cend(a)); // == 6
// 使用自定义二元运算符:
std::reduce(std::cbegin(a), std::cend(a), 1, std::multiplies{}); // == 6
此外,还可以为归约器指定转换操作:
std::transform_reduce(std::cbegin(a), std::cend(a), 0, std::plus{}, times_ten); // == 60
const std::arrayint, 3> b{ 1, 2, 3 };
const auto product_times_ten = [](const auto a, const auto b) { return a * b * 10; };
std::transform_reduce(std::cbegin(a), std::cend(a), std::cbegin(b), 0, std::plus{}, product_times_ten); // == 140
前缀和算法支持前缀和(包括包含扫描和排除扫描)以及转换。
const std::arrayint, 3> a{ 1, 2, 3 };
std::inclusive_scan(std::cbegin(a), std::cend(a),
std::ostream_iteratorint>{ std::cout, " " }, std::plus{}); // 1 3 6
std::exclusive_scan(std::cbegin(a), std::cend(a),
std::ostream_iteratorint>{ std::cout, " " }, 0, std::plus{}); // 0 1 3
constauto times_ten = [](constauto n) { return n * 10; };
std::transform_inclusive_scan(std::cbegin(a), std::cend(a),
std::ostream_iteratorint>{ std::cout, " " }, std::plus{}, times_ten); // 10 30 60
std::transform_exclusive_scan(std::cbegin(a), std::cend(a),
std::ostream_iteratorint>{ std::cout, " " }, 0, std::plus{}, times_ten); // 0 10 30
最大公约数和最小公倍数最大公约数(GCD)和最小公倍数(LCM)。
const int p = 9;
const int q = 3;
std::gcd(p, q); // == 3
std::lcm(p, q); // == 9
std::not_fn实用函数,返回给定函数结果的否定。
const std::ostream_iteratorint> ostream_it{ std::cout, " " };
const auto is_even = [](const auto n) { return n % 2 == 0; };
std::vectorint> v{ 0, 1, 2, 3, 4 };
// 打印所有偶数。
std::copy_if(std::cbegin(v), std::cend(v), ostream_it, is_even); // 0 2 4
// 打印所有奇数(非偶数)。
std::copy_if(std::cbegin(v), std::cend(v), ostream_it, std::not_fn(is_even)); // 1 3
字符串与数字的相互转换将整数和浮点数转换为字符串,反之亦然。这些转换是不抛出异常的,不会进行分配,并且比 C 标准库中的等效函数更安全。
用户负责为 std::to_chars 分配足够的存储空间,否则函数将通过在其返回值中设置错误代码对象来失败。
这些函数允许您可选地传递基数(默认为 10 进制)或浮点输入的格式说明符。
std::to_chars 返回一个(非 const)字符指针,指向函数在给定缓冲区内写入的字符串的末尾,以及一个错误代码对象。std::from_chars 返回一个 const 字符指针,成功时等于传递给函数的结束指针,以及一个错误代码对象。这两个函数返回的错误代码对象在成功时都等于默认初始化的错误代码对象。
将数字 123 转换为 std::string:
const int n = 123;
// 可以使用任何容器,字符串,数组等。
std::string str;
str.resize(3); // 为 `n` 的每个数字分配足够的存储空间
const auto [ ptr, ec ] = std::to_chars(str.data(), str.data() + str.size(), n);
if (ec == std::errc{}) { std::cout std::endl; } // 123
else { /* 处理失败 */ }
从值为 "123" 的 std::string 转换为整数:
const std::string str{ "123" };
int n;
const auto [ ptr, ec ] = std::from_chars(str.data(), str.data() + str.size(), n);
if (ec == std::errc{}) { std::cout std::endl; } // 123
else { /* 处理失败 */ }
chrono 持续时间和时间点的舍入函数为 std::chrono::duration 和 std::chrono::time_point 提供 abs、round、ceil 和 floor 辅助函数。
using seconds = std::chrono::seconds;
std::chrono::milliseconds d{ 5500 };
std::chrono::abs(d); // == 5s
std::chrono::round(d); // == 6s
std::chrono::ceil(d); // == 6s
std::chrono::floor(d); // == 5s
致谢cppreference - 特别适用于查找新库特性的示例和文档。C++ Rvalue References Explained - 我用来理解右值引用、完美转发和移动语义的绝佳入门资料。clang 和 gcc 的标准支持页面。这里还包含了我用来查找语言/库特性描述、其目的以及一些示例的提案。Compiler explorerScott Meyers 的 Effective Modern C++ - 高度推荐的书籍!Jason Turner 的 C++ Weekly - 一个很好的 C++ 相关视频合集。What can I do with a moved-from object?What are some uses of decltype(auto)?还有许多我忘记的 Stack Overflow 帖子……本文翻译自https://github.com/AnthonyCalandra/modern-cpp-features/blob/master/CPP17.md现在不少朋友都在准备校招,常规的技术学习只是提高了代码能力,还没有提升从 0 到 1 整体做项目和解决问题的能力!
为此我们特别推出了C++训练营,带着你从 0 到 1 做 C++ 项目(你可以从项目池中任选项目),帮助你提升做项目的能力,提升从0到1的能力,熟悉做项目的完整流程,比如开发环境、编译脚本、架构设计、框架搭建、代码发布、问题调试、单元测试。
除了上面的,还有需求分析、项目规划、架构设计、任务拆解、时间规划、版本管理。另外做项目的过程中必然会遇到种种问题,可以逐步提升你的调试能力,分析问题的能力,掌握更多的调试手段。
遇到棘手的问题,我们还有专门的导师1V1答疑解惑,给出具体建议。
项目池里面的项目,是导师团队花费大量时间完成的,不仅有完整的代码及清晰的注释还有详细的文档和视频,并且有专门的项目导师答疑,完全不用担心自己学不会。
相信我,这些项目绝对能够让你进步巨大!下面是其中某三个项目的说明文档 |
|