相对于C++14,C++17是一个大的更新版本,引入了许多新的特性帮助开发者更方便地编写代码。截止到目前,编译器对C++17支持已经非常完善,因此值得全面拥抱C++17及以上标准用于日常开发。
经过诸多实践后,本文总结一些能简化代码的C++17特性,让你写出更简洁易懂可维护的代码。
C++17简化代码编写的新特性
1. 内联变量(inline variables)
C++17之前,头文件中定义全局变量不能轻易赋值(const
和 constexpr
修饰例外),或者建议用 extern
关键字声明。当然现在全局变量用的少,烦恼转到了类的 static
成员变量上:明明类中定义或者初始化就好了,为什么非要在cpp文件中重新来一遍呢(还要加上类名和冒号)?
C++17中引入了内联变量功能, inline
关键字也可以用来修饰变量。有了该功能,头文件中也可以直接声明并定义全局变量、类静态变量,不必在cpp中又重复一遍,对header only的库更为友好。
struct MyData { inline static MyData *instance = nullptr; // 声明并定义,不用跑到cpp文件中再写一遍 };
2. 结构化绑定(structured bindings)
之前在遍历 map/hashtable
时,需要先得到 pair
,然后再解包使用:
for (auto &pair : map) { auto &key = pair.first; auto &value = pair.second; }
有了结构化绑定功能后,可以直接解包 pair
,写法一下就简单了:
for (auto &[key, value] : map { ... }
类似的,常用的 tuple 结构也可以直接解包,不用再 get<0>、get<1> 这样别扭的访问数据:
for (auto &[id, age, name] : students) { ... }
结构化绑定不仅适用于 pair、tuple 这些类型,对于聚合结构体和类(数据成员要求都是 public
)都是适用的:
struct Student { int id; int age; std::string name; }; Student s1 {1, 18, "Anna"}; auto &[id, age, name] = s1; ...
3. 带初始化的if/switch(initializers for if and switch)
有时需要定义临时变量进行分支判断,如果想限制临时变量的作用域范围需要定义新代码块:
... { auto res = check(); if (res) { } } ...
有了带初始化if功能,可以在 if 语句中定义临时变量,其作用域在整个 if
中有效,包括 else
分支:
if (auto res = check(); res) { } else { // res 在这里依然有效 } // res在这里无效
带初始化的 switch
语句功能和用法类似。
4. 类模板参数推导(class template argument deduction)
在C++17之前,使用类模板必须显式指定所有模板参数:
std::unordered_map<int, int> id2idx {{1, 0}, {2, 3}};
C++17近一步简化了使用,只要编译器能根据初始值推导出所有模板参数,则模板参数可以省略:
std::unordered_map id2idx {{1, 0}, {2, 3}};
可以看到变量定义和声明又简化了,不少现代化的代码都是 std::pair
、std::vector
而不指定模板参数了。
5. 编译期if语句(compile-time if constexpr)
虽然 C++模板编程 功能一直在增强,但其实门槛还是比较高的。(偏)特化、SFINAE
、std::enable_if
等概念晦涩难懂,许多模板代码仅在细节上有轻微差异,而且还很难debug。C++17引入的编译期if语句,大大简化了模板编程,非常实用。
考虑两个向量的点积,在C++17之前需要通过特化等手段来实现:
template<typename T> double dot(const std::vector<T> &v, const std::vector<T> &w) { assert(v.size() == w.size()); auto size = v.size(); auto res = .0; for (size_t i = 0; i < size; ++ i) { res += v[i] * w[i]; } return res; } // complex版本 template<> double dot(const std::vector<std::complex<double>> &v, const std::vector<std::complex<double>> &w) { assert(v.size() == w.size()); auto size = v.size(); auto res = .0; for (size_t i = 0; i < size; ++ i) { res += std::abs(v[i] * w[i]); } return res; }
可以看到,上面其实有很多重复的代码(当然可以通过其它方式消除)。C++17带来的编译期if语句功能则能很好的简化上面的代码:
template<typename T> double dot(const std::vector<T> &v, const std::vector<T> &w) { assert(v.size() == w.size()); auto size = v.size(); auto res = .0; for (size_t i = 0; i < size; ++ i) { if constexpr (std::is_same_v(T, double)) { res += v[i] * w[i]; } if constexpr (std::is_same_v(T, std::complex<double>)) { res += std::abs(v[i] * w[i]); } } return res; }
简化后的函数更像正常的函数,并且可读性大幅提高。
6. 折叠表达式(fold expressions)
在C++11中,变长模板参数展开需要提供同名函数来终止递归,例如删除对象:
template<typename T> void Delete(T *p) { delete p; } template<typename T, typename... Args> void Delete(T *p, Args... args) { Delete(p); Delete(args...); }
既然操作都一样的,为什么需要单独写一个函数呢?C++17引入折叠表达式简化变长模板参数编程:
template<typename... Args> void Delete(Args... args) { ((delete args), ...); }
可以看到,操作一致,并且代码量大幅减少。
7. 文件系统(file system)
文件系统是标准库的增强,通过文件系统库,文件及文件夹相关操作再也无需对不同的操作系统写好几套代码。
需要注意的是,一些相对旧的编译器文件系统相关文件是在 exprimental
名字空间下,可以使用如下代码进行兼容:
#if __has_include(<filesystem>) #include <filesystem> #else #include <experimental/filesystem> namespace std { namespace filesystem = experimental::filesystem; } #endif
总结
C++17带来的新功能特性远不止上文所说,但是就编程体验而言,多使用上面的几条特性能让你的代码简化不少,非常推荐使用。
参考
1. C++17
2. C++17完全指南