C++17 新增特性总结: 核心语言特性
语言特性
结构化绑定(Structured Bindings)
结构化绑定允许你将一个结构体, 元组或数组的元素直接绑定到命名变量上, 下面是几个 C++17 结构化绑定的示例:
// struct binding
struct Item {
int id;
std::string name;
};
Item item = {1, "item"};
auto [id, name] = item;
std::map<std::string, int> scores = {
{"Alice", 85}, {"Bob", 90}, {"Charlie", 78}};
// 绑定std::pair
for (const auto& [key, value] : scores) {
std::cout << key << ": " << value << '\n';
}
// 绑定tuple
auto t1 = std::tuple<int, int, int>{1, 2, 3};
auto [x, y, z] = t1;
带初始化的 if
/switch
在下面的例子中, if
语句中声明了一个局部变量it
, 并且在if
语句块中使用这个变量.
// if 语句中初始化变量
if (auto it = scores.find("Bob"); it != scores.end()) {
std::cout << "找到了元素 Bob" << std::endl;
}
// 在 switch 语句中初始化变量
switch (auto score = scores["Bob"]; score / 10) {
case 9:
std::cout << "Bob 的成绩等级是 A" << std::endl;
break;
case 8:
std::cout << "Bob 的成绩等级是 B" << std::endl;
break;
case 7:
std::cout << "Bob 的成绩等级是 C" << std::endl;
break;
default:
std::cout << "Bob 的成绩等级未知" << std::endl;
}
inline 变量的增强
C++17 增加了两个新的特性:
- 在头文件中用
inline
修饰变量, 而不用担心重复定义的问题. - 在类中声明
static inline
成员变量时, 可以直接初始化.
#pragma once
class sample {
inline static int default_value = 47; /* C++ 17支持内联定义, 无需分开初始化 */
};
inline int global_uuid = 0; /* C++ 17新特性, 被多个文件include也不会出现冲突 */
更多类初始化的内容: C++ 类成员初始化发展历程(从 C++11 到 C++20)
聚合扩展
在 C++17 之前, 从其他结构派生的结构会禁用聚合初始化, 开发者必须定义构造函数. C++17 引入聚合体扩展后, 聚合体可以拥有基类, 支持使用花括号进行列表初始化, 并且可以省略一些嵌套括号, 使代码更加简洁.
struct Data {
std::string name;
double value;
};
struct MoreData : Data {
bool done;
};
MoreData md1{{"test1", 6.778}, false}; // 使用嵌套括号进行初始化
MoreData md2{"test1", 6.778, false}; // 当基类或子对象只接受一个值时, 可省略嵌套括号
mandatory copy elision or parsing unmaintainable code
在 C++17 之前, 复制省略(Copy Elision)是一种编译器优化手段, 允许编译器在某些情况下省略对象的复制或移动操作, 但这并非强制要求, 代码仍然需要保证复制构造函数和移动构造函数的可调用性. 而在 C++17 中, 在特定的初始化场景下, 复制省略成为强制规则, 即使类没有定义复制或移动构造函数, 相关的初始化操作也能正常进行.
强制复制省略的场景:
- 返回临时对象: 当函数按值返回一个临时对象(prvalue)时, 强制复制省略会发生, 直接在调用处初始化目标对象, 而不会进行复制或移动操作.
- 直接初始化: 使用临时对象直接初始化另一个对象时, 也会触发强制复制省略.
class MCE {
int x;
public:
MCE() { std::cout << "Constructor called" << std::endl; }
// 显式删除复制构造函数
MCE(const MCE&) = delete;
// 显式删除移动构造函数
MCE(MCE&&) = delete;
~MCE() { std::cout << "Destructor called" << std::endl; }
};
auto createObject = []() { return MCE(); };
MCE mce2 = createObject(); // 返回临时对象
MCE mce1 = MCE(); // 直接初始化
lambda 表达式增强
constexpr
lambda
auto l1 = [](auto x) constexpr { return x * x; }; // OK
constexpr int ci1 = l1(2);
lambda 捕获 *this
class C {
private:
std::string name;
public:
void foo() {
auto l1 = [*this] { std::cout << name << '\n'; };
}
};
命名空间增强
- 嵌套的命名空间
// before
namespace Company {
namespace Project {
namespace Component {
class Foo;
} // namespace Component
} // namespace Project
} // namespace Company
// c++ 17
namespace Company::Project::Component {
class Foo;
}
新增 attributes
-
[[nodiscard]]
: 用于标记函数返回值不应该被忽略. 如果忽略带有[[nodiscard]]
属性的函数返回值, 编译器通常会给出警告, 有助于避免因忽略重要返回值而可能导致的逻辑错误.[[nodiscard]] int openFile(const char* path) { int fd = -1; // ... return fd; }
-
[[fallthrough]]
: 用于在switch
语句中显式表明允许从一个case
标签穿透下降到下一个case
标签, 避免编译器给出警告.void comment(int place) { switch (place) { case 1: std::cout << "非常 "; [[fallthrough]]; case 2: std::cout << "好\n"; break; default: std::cout << "一般\n"; break; } }
-
[[maybe_unused]]
: 用于告诉编译器某个变量, 函数参数, 类成员等可能不会被使用, 从而避免编译器给出未使用变量的警告.void processData([[maybe_unused]] int value, double factor) { // 这里没有使用 value 参数,但由于 [[maybe_unused]] 标记,不会产生警告 double result = factor * 2; std::cout << "Result: " << result << std::endl; }
utf-8 character literals
从 C++11 开始有string
类型的字面量, C++17 引入了 utf-8 字符串字面量.
// u8 char literals
char c = u8'a'; // ASCII
char16_t ch = u'猫'; // 中文字符
char32_t emoji = U'🍌'; // emoji
noexcept
specifications are part of the type system
在 C++17 中, noexcept
关键字被引入了类型系统中, 意味着是否有noexcept
将影响函数类型.
void f1(int x) noexcept;
void f2(int x);
f1
和f2
是不同的类型.
class Base {
public:
virtual void foo() noexcept;
};
class Derived : public Base {
public:
void foo() override; // ERROR: does not override
};
表达式求值顺序
C++17 中, 规定了表达式求值顺序如下:
- 如下形式的表达式中,
e1
先于e2
求值e1[e2]
e1.e2
e1.*e2
e1->*e2
e1 << e2
e1 >> e2
- 赋值语句中, 右侧的表达式先求值
e2 = e1
e2 += e1
e2 *= e1
new Type(e)
操作符中, 先分配内存, 再计算参数e
样例代码
i = ++i + 2; // well-defined
i = i++ + 2; // undefined behavior until C++17
a[i] = i++; // undefined behavior until C++17
std::cout << i << i++; // undefined behavior until C++17
i = ++i + i++; // undefined behavior
int n = ++i + i; // undefined behavior
relaxed enum initialization from integral types
是 C++17 引入的一项新特性,主要用于改进枚举类型的初始化方式。在 C++17 之前,对于具有固定底层类型的枚举,使用整数值进行直接列表初始化是不允许的,而 C++17 放宽了这一限制。
// 无作用域枚举,指定底层类型为char
enum Enum1 : char {};
Enum1 i1{42}; // OK since C++17 (ERROR before C++17)
Enum1 i2 = 42; // 仍然错误
Enum1 i3(42); // 仍然错误
Enum1 i4 = {42}; // 仍然错误
// 有作用域枚举,默认底层类型
enum class Enum2 { mon, tue, wed, thu, fri, sat, sun };
Enum2 s1{0}; // OK since C++17 (ERROR before C++17)
Enum2 s2 = 0; // 仍然错误
Enum2 s3(0); // 仍然错误
Enum2 s4 = {0}; // 仍然错误
// 有作用域枚举,指定底层类型为char
enum class Enum3 : char { mon, tue, wed, thu, fri, sat, sun };
Enum3 s5{0}; // OK since C++17 (ERROR before C++17)
Enum3 s6 = 0; // 仍然错误
Enum3 s7(0); // 仍然错误
Enum3 s8 = {0}; // 仍然错误
// 无作用域枚举,未指定底层类型
enum Enum4 { bit1 = 1, bit2 = 2, bit3 = 4 };
Enum4 f1{0}; // 仍然错误
// 尝试进行窄化转换,仍然错误
enum Enum5 : char {};
Enum5 i5{42.2}; // 仍然错误
改进auto
直接初始化行为
auto
类型推到的行为在 C++17 中进行了修改:
// C++ 17 之前
auto at1{42}; // 类型为: std::initializer_list<int>
auto at2{1,2,3}; // OK: 类型为: std::initializer_list<int>
// C++17
auto at1{42}; // 类型为int
auto at2{1, 2, 3}; // Error
单个参数的 static_assert
// C++17 之前
static_assert(std::is_default_constructible<T>::value, "class C: elements must be default-constructible");
// C++17
static_assert(std::is_default_constructible_v<T>);
预处理宏 __has_include
检查特定的头文件是否能被包含. 也就是检查系统是否存在该头文件.
#if __has_include(<filesystem>)
#include <filesystem>
#define HAS_FILESYSTEM 1
#elif __has_include(<experimental/filesystem>)
#include <experimental/filesystem>
#define HAS_FILESYSTEM 1
#define FILESYSTEM_IS_EXPERIMENTAL 1
#elif __has_include("filesystem.hpp")
#include "filesystem.hpp"
#define HAS_FILESYSTEM 1
#define FILESYSTEM_IS_EXPERIMENTAL 1
#else
#define HAS_FILESYSTEM 0
#endif