C++17新特性

归纳总结一些常见的(我见过的)或比较好理解的(我能理解的)C++17的特性,具体的全部特性参考链接C++17 - cppreference.com

New language features

Variables

结构化绑定

首先先举一个例子,在leetcode刷题看题解经常会遇到这种写法。

1
2
3
4
5
6
7
unordered_map<int, int> mp;
for (auto &[k , v] : mp) {
if (k == 1) {//获取值
v = 1;//修改值
}
...
}

其中的 auto [k, v]就是结构化绑定,通过结构化绑定,我们可以很方便地获取 map对应的值。

除去 map之外,还可以应用于 pair,tuple,结构体和数组,如果加上引用,还可以修改对象的值。

还有就是,可以实现自定义类的结构化绑定以及结构化绑定不能应用于constexpr(据说c++20可以),我不懂就简单提一下。

if-switch 语句初始化

举个例子,c++17前,

1
2
3
4
5
6
7
8
9
int a = getValue();
if (a > 10) {
//to something
}

switch(a) {
case 1:
default:
}

c++17后。

1
2
3
4
5
6
7
8
if (int a = getValue();a > 10) {
//to something
}

switch(int a = getValue();a) {
case 1:
default:
}

内联变量

在头文件里定义一个内联的变量或者对象,如果这个定义被多个编译单元使用,那么他们都指向同一个唯一的对象。

1
2
3
4
5
6
7
//header file
class MyClass
{
static inline std::string name = ""; // OK since C++17
//...
};
inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files

引入的原因,首先是c++中,不允许在类中初始化非const静态成员;

因为静态成员是属于类,而不属于某个对象,如果在类中初始化,会导致每个对象都包含该静态成员。

其次是如果在被多个cpp引用的头文件中定义类结构之外的变量会导致这些cpp中都定义了这个变量,导致重定义变量。

关于内联就可以再写一篇文章,这就是学习c++让我感到痛苦的地方,平常你轻视的地方,却蕴含着三言两语难以说清楚的门道。

Templates

类模版参数推导(CATD/class template argument deduction)

1
2
3
4
//before c++17
std::vector<FooBar<int, const char*>> obj{a, b, c};
//c++17
std::vector obj{a, b, c};

constexpr

constexpr lambda表达式

C++17允许lambda函数成为constexpr,如果它们满足条件,就可以在需要编译时评估的上下文中使用

1
2
constexpr auto lambda = [](int x) { return x * 2; };
static_assert(lambda(5) == 10);

编译期if(compile-time if constexpr)

if constexpr是编译期的判断语句

1
2
3
4
5
6
7
8
9
10
//不同输入类型转换为字符串
template <typename T>
std::string convert(T input) {
if constexpr (std::is_same_v<T, const char*> ||
std::is_same_v<T, std::string>) {
return input;
} else {
return std::to_string(input);
}
}

Namespaces

简化的命名空间嵌套(simplified nested namespaces)

1
2
3
4
5
6
7
8
9
10
11
12
namespace A {
namespace B {
namespace C {
void func();
}
}
}

// c++17,更方便更舒适
namespace A::B::C {
void func();)
}

__has_include预处理表达式

用来判断是否有某个头文件,代码可能在不同编译器下工作,不同编译器的可用头文件有可能不同。

新增Attribute

首先 Attribute是一个关键字,用于指定一个函数、变量、类、模板或类型 trait 应该具有的特殊行为。

[[fallthrough]]

通常在使用 switch 语句时,如果case 后没有加break,编译器就会发出警告信息,在case处理部分添加这个属性,用于消除这个警告信息,表示这部分逻辑本意如此。此外该属性只能用在 switch 语句中。

1
2
3
4
5
6
7
8
9
10
switch (i) {}
case 1:
xxx; // warning
case 2:
xxx;
[[fallthrough]];// 警告消除
case 3:
xxx;
break;
}

[[nodiscard]]

被此属性修饰的函数,其返回值不应该被丢弃,如果被丢弃编译器就会发出警告信息;如果是修饰的枚举或者类,那么在对应函数返回该类型的时候也不应该丢弃结果。

1
2
3
4
[[nodiscard]] int func();
void F() {
func(); // warning 没有处理函数返回值
}

[[maybe_unused]]

通常如果声明了一个变量但是从来没有使用过,编译器就会发出警告信息,使用该属性之后,编译器就会认为是故意为之,从而不再发出警告。需要注意,这个声明不会影响编译器的优化逻辑,在编译优化阶段,无用的变量还是会被处理掉。

lambda通过*this捕获对象副本(lambda capture of *this)

正常情况下,lambda捕获了this指针,如果this指向的对象析构了,而函数再被调用且访问了成员变量,就会有问题,结果往往是崩溃。

这个新特性让你捕获*this,持有了对象的拷贝,从而避免了上述问题。

1
2
3
4
5
6
7
8
9
struct A {
int a;
void func() {
auto f = [*this] { // 这里
cout << a << endl;
};
f();
}
};

New library features

Utility types

  • std::any适用于之前使用void*作为通用类型的场景。
  • std::optional适用于之前使用nullptr代表失败状态的场景。
  • std::variant适用于之前使用union的场景。

std::any

可以存储任意类型的单个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main() {
std::any a = std::make_any<int>(1);//使用std::make_any创建对象
cout << a.type().name() << " " << std::any_cast<int>(a) << endl;//使用std::any_cast获取值
a = 2.2f;
cout << a.type().name() << " " << std::any_cast<float>(a) << endl;
if (a.has_value()) {//使用has_value()判断是否有值
cout << a.type().name();
}
a.reset();//销毁所含对象
if (a.has_value()) {
cout << a.type().name();//type()查询所含的类型,返回typeid
}
a = emplace<std::string>("a");//使用emplace会销毁之前的对象,构造新的对象
return 0;
}

std::optional

见名知意,表示一个值可能存在,没有值就是默认的 std::nullopt

1
2
3
4
5
6
7
std::optional<int> divide(int a, int b) {
if (b == 0) {
return std::nullopt;
} else {
return a / b;
}
}

std::optional对象使用 has_valut()来判断是否有值,使用 *或者value()来取值

std::variant

类型安全的联合体(可以称之为变化体),功能上与union类似,但是更加高级。

  • 类型安全,由于存储了内部的类型信息,所有可以进行安全的类型转换
  • 可以存储复杂类型,union只能存储POD类型(Plain Old Date)

Memory management

共享指针支持动态数组(array support for std::shared_ptr)

1
std::shared_ptr<uint8_t[]> sp(new uint8_t[extraDataLength_], [](uint8_t* ptr) { delete[] ptr; });

Compile-time programming(to-do)

Algorithms

并行算法

c++17支持STL并行执行,简单提一下,以std::sort为例

1
std::sort(exe_policy, begin, end, comp);

可以添加如下三种的执行策略

  • std::execution::seq(顺序执行)
  • std::execution::par(并行执行)
  • std::execution::par_unseq(并行和向量化执行)

Iterators and containers

std::map/unordered_map try_emplace

std::map/unordered_map中插入元素往往使用emplace,其操作是如果元素不存在就会插入元素,否则不插入,但是如果元素已经存在,此时仍会构造一次待插入的元素,在判断不需要插入后将该元素立刻析构,所以产生了额外的开销, try_emplace就避免了这种问题。

Others

std::string_view

通常我们传递一个string时会触发对象的拷贝操作,大字符串的拷贝赋值操作会触发堆内存分配,很影响运行效率,有了string_view就可以避免拷贝操作,平时传递过程中传递string_view即可。

1
2
3
4
5
6
7
8
9
10
11
void func(std::string_view stv) { cout << stv << endl; }

int main(void) {
std::string str = "Hello World";
std::cout << str << std::endl;

std::string_view stv(str.c_str(), str.size());
cout << stv << endl;
func(stv);
return 0;
}

std::shared_mutex