相对于C++14,C++17是一个大的更新版本,引入了许多新的特性帮助开发者更方便地编写代码。截止到目前,编译器对C++17支持已经非常完善,因此值得全面拥抱C++17及以上标准用于日常开发。

经过诸多实践后,本文总结一些能简化代码的C++17特性,让你写出更简洁易懂可维护的代码。

C++17简化代码编写的新特性

1. 内联变量(inline variables)

C++17之前,头文件中定义全局变量不能轻易赋值(constconstexpr 修饰例外),或者建议用 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) {
  ...
}

结构化绑定不仅适用于 pairtuple 这些类型,对于聚合结构体和类(数据成员要求都是 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::pairstd::vector 而不指定模板参数了。

5. 编译期if语句(compile-time if constexpr)

虽然 C++模板编程 功能一直在增强,但其实门槛还是比较高的。(偏)特化、SFINAEstd::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完全指南

3. Simplify Code with “if constexpr” in C++17