C++17 新增特性总结

本文对 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 增加了两个新的特性:

  1. 在头文件中用inline修饰变量, 而不用担心重复定义的问题.
  2. 在类中声明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};   // 当基类或子对象只接受一个值时, 可省略嵌套括号

强制省略拷贝

在 C++17 之前, 复制省略(Copy Elision)是一种编译器优化手段, 允许编译器在某些情况下省略对象的复制或移动操作, 但这并非强制要求, 代码仍然需要保证复制构造函数和移动构造函数的可调用性.

而在 C++17 中, 在特定的初始化场景下, 复制省略成为强制规则, 即使类没有定义复制或移动构造函数, 相关的初始化操作也能正常进行.

强制复制省略的场景:

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

utf-8 字符字面量

从 C++11 开始有 string 类型的字面量, C++17 引入了 utf-8 字符串字面量.

// u8 char literals
char c = u8'a';          // ASCII
char16_t ch = u'猫';     // 中文字符
char32_t emoji = U'🍌';  // emoji

更多关于字面量的内容: Modern C++ 字面量一网打尽

noexcept 限定符成为类型系统的一部分

在 C++17 中, noexcept关键字被引入了类型系统中, 意味着是否有noexcept将影响函数类型.

void f1(int x) noexcept;
void f2(int x);

f1f2是不同的类型.

class Base {
  public:
  virtual void foo() noexcept;
};
class Derived : public Base {
  public:
  void foo() override;  // ERROR: does not override
};

表达式求值顺序

C++17 中, 规定了表达式求值顺序如下:

  1. 如下形式的表达式中, e1先于e2求值
    • e1[e2]
    • e1.e2
    • e1.*e2
    • e1->*e2
    • e1 << e2
    • e1 >> e2
  2. 赋值语句中, 右侧的表达式先求值
    • e2 = e1
    • e2 += e1
    • e2 *= e1
  3. 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

放松枚举从整型初始化的限制

在 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

hexadecimal floating literals

单个参数的 static_assert

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

模板编程

类模板参数推导

Class Template Argument Deduction (CTAD) 允许编译器根据传递给构造函数的参数自动推导模板参数类型, 从而简化了模板类的使用. 在此之前, 必须显式指定所有模板参数类型. CTAD 支持多种初始化方式, 并且可以用于函数模板, 类模板和通用库.

编译时 if

编译时 if(Compile-Time if) 允许在编译时根据条件选择执行不同的代码分支. 与运行时的 if 语句不同, if constexpr 只会在编译时评估其条件, 并且只有满足条件的分支会被实例化和编译, 不满足条件的分支则会被完全忽略. 这一特性在模板编程中特别有用, 因为它可以避免无效代码路径导致的编译错误.

类型转换和处理

根据传入参数的类型自动选择合适的处理逻辑, 例如字符串, 整数或浮点数的不同处理方式.

template <typename T>
std::string asString(T x) {
  if constexpr (std::is_same_v<T, std::string>) {
    return x;  // 直接返回字符串类型
  } else if constexpr (std::is_arithmetic_v<T>) {
    return std::to_string(x);  // 转换为字符串
  } else {
    return std::string(x);  // 尝试转换为字符串
  }
}

返回值完美转发

当需要对返回值进行处理后再返回时, 可以根据返回类型是否为 void 来决定如何处理.

template <typename Callable, typename... Args>
decltype(auto) call(Callable op, Args&&... args) {
  if constexpr (std::is_void_v<std::invoke_result_t<Callable, Args...>>) {
    // 返回类型是 void
    op(std::forward<Args>(args)...);
    // 执行其他操作...
    return;
  } else {
    // 返回类型不是 void
    decltype(auto) ret{op(std::forward<Args>(args)...)};
    // 执行其他操作...
    return ret;
  }
}

标签调度

可以在一个函数内实现多态行为, 而不需要为每种类型提供重载函数.

template <typename Iterator, typename Distance>
void Advance(Iterator& pos, Distance n) {
  using cat = std::iterator_traits<Iterator>::iterator_category;
  if constexpr (std::is_same_v<cat, std::random_access_iterator_tag>) {
    pos += n;
  } else if constexpr (std::is_same_v<cat, std::bidirectional_iterator_tag>) {
    if (n >= 0) {
      while (n--) {
        ++pos;
      }
    } else {
      while (n++) {
        --pos;
      }
    }
  } else {  // input_iterator_tag
    while (n--) {
      ++pos;
    }
  }
}

更多阅读:


折叠表达式(Fold Expressions)

Fold Expressions (折叠表达式) 提供了一种简洁的方法来对参数包(parameter pack)中的所有元素应用二元操作符. 这项特性极大地简化了模板编程中对变长参数列表的操作, 避免了递归模板实例化带来的复杂性和性能开销.

基本概念

应用场景

将字符串作为模板参数

C++17 允许在模板中使用字符串字面量(string literals)作为非类型模板参数. 这项特性扩展了模板编程的能力, 使得可以更灵活地处理字符串常量.

#include <iostream>

// 定义一个模板类, 接受一个常量字符指针作为模板参数
template <const char* str>
class Message {
 public:
  void print() const { std::cout << str << std::endl; }
};

// 定义字符串变量, 分别具有外部链接, 内部链接
extern const char external[] = "Hello, World!";  // 外部链接
const char internal[] = "Hello, Internal!";      // 内部链接

int main() {
  // 使用具有外部链接的字符串, 所有版本都支持
  Message<external> msg_external;  // all version
  msg_external.print();            // 输出: Hello, World!

  // 使用具有内部链接的字符串(C++11及更高版本)
  Message<internal> msg_internal;
  msg_internal.print();  // 输出: Hello, Internal!

  // 使用无链接的字符串(仅在C++17及以上版本支持)
  static const char hello_world_no_link[] = "Hello, No Link!";  // 无链接
  Message<hello_world_no_link> msg_no_link;
  msg_no_link.print();  // 输出: Hello, No Link!

  return 0;
}

auto 作为模板参数

C++17 允许在模板中使用auto关键字来声明非类型的模板参数. 这项特性使得模板可以接受不同类型和值的参数, 从而增强了模板编程的灵活性和通用性.

基本概念

  1. 应用场景

    • 编译时常量表达式的简化: 可以通过auto更简洁地定义编译时常量.
    • 变量模板中的应用: 可以在变量模板中使用auto作为模板参数, 进一步增强模板的灵活性.

使用auto作为非类型模板参数

通过使用auto, 可以避免为每个可能的类型编写多个模板特化版本.

#include <iostream>

// 定义一个模板类, 使用auto作为非类型模板参数
template<auto N>
class S {
public:
    static void print() {
        std::cout << "Value: " << N << ", Type: " << typeid(N).name() << std::endl;
    }
};

int main() {
    // 使用不同类型的常量初始化模板
    S<42> s1;  // int 类型
    S<'a'> s2; // char 类型
    // S<2.5> s3; // 错误: double 类型不被支持

    s1.print(); // 输出: Value: 42, Type: i
    s2.print(); // 输出: Value: a, Type: c

    return 0;
}

使用auto定义编译时常量

#include <iostream>

// 定义一个模板结构体, 使用auto作为模板参数
template <auto v>
struct Constant {
  static constexpr auto value = v;
};

int main() {
  // 使用不同的常量初始化模板
  using IntConstant = Constant<42>;
  using CharConstant = Constant<'x'>;
  using BoolConstant = Constant<true>;

  std::cout << "IntConstant: " << IntConstant::value
            << std::endl;  // 输出: IntConstant: 42
  std::cout << "CharConstant: " << CharConstant::value
            << std::endl;  // 输出: CharConstant: x
  std::cout << "BoolConstant: " << BoolConstant::value
            << std::endl;  // 输出: BoolConstant: 1

  return 0;
}

扩展的 using 声明(Extended Using Declarations)

C++17 允许在类定义中使用逗号分隔的列表来导入多个基类成员. 这项特性简化了代码编写, 减少了冗余, 并提高了代码的可读性和维护性.

功能特性

示例 1: 多基类成员导入

可以在派生类中通过一个using声明导入多个基类成员, 而不需要为每个成员单独写一个using声明.

#include <iostream>

class Base {
 public:
  void a() { std::cout << "Base::a\n"; }
  void b() { std::cout << "Base::b\n"; }
  void c() { std::cout << "Base::c\n"; }
};

class Derived : private Base {
 public:
  // 使用逗号分隔的using声明导入多个基类成员
  using Base::a, Base::b, Base::c;
};

int main() {
  Derived d;
  d.a();  // 输出: Base::a
  d.b();  // 输出: Base::b
  d.c();  // 输出: Base::c
}

示例 2: Lambda 重载集

通过使用using声明, 可以从传递的基础类型中"继承"所有函数调用操作符, 从而创建一组 lambda 重载.

#include <iostream>
#include <string>

// 定义一个结构体模板, 用于"继承"所有函数调用操作符
template <typename... Ts>
struct Overload : Ts... {
  using Ts::operator()...;  // 使用using声明使所有操作符可用
};

// 基类型推导指南
template <typename... Ts>
Overload(Ts...) -> Overload<Ts...>;

int main() {
  // 创建一个包含两个lambda表达式的Overload对象
  auto twice =
      Overload{[](std::string& s) { s += s; }, [](auto& v) { v *= 2; }};

  int i = 42;
  twice(i);                         // 调用第二个lambda
  std::cout << "i: " << i << '\n';  // 输出: i: 84

  std::string s = "hi";
  twice(s);                         // 调用第一个lambda
  std::cout << "s: " << s << '\n';  // 输出: s: hihi
}

示例 3: 构造函数继承

可以通过using声明继承基类的所有构造函数, 减少重复代码.

#include <iostream>
#include <string>

template <typename T>
class Base {
  T value{};

 public:
  Base() : value{} {}
  Base(T v) : value{v} {}

  void print() const { std::cout << "Value: " << value << '\n'; }
};

template <typename... Types>
class Multi : private Base<Types>... {
 public:
  // 继承所有基类的构造函数
  using Base<Types>::Base...;

  // 提供一个统一的 print 接口
  void print() const {
    // 使用折叠表达式依次调用每个基类的 print 函数
    (static_cast<Base<Types> const*>(this)->print(), ...);
  }

  // 提供特定类型的 print 函数
  template <typename T>
  void print_specific() const {
    static_cast<Base<T> const*>(this)->print();
  }
};

int main() {
  // 使用Multi<> 类型声明三个不同类型的对象
  using MultiISB = Multi<int, std::string, bool>;

  MultiISB m1 = 42;                    // 调用Base<int>的构造函数
  MultiISB m2 = std::string("hello");  // 调用Base<std::string>的构造函数
  MultiISB m3 = true;                  // 调用Base<bool>的构造函数

  m1.print_specific<int>();          // 输出: Value: 42
  m2.print_specific<std::string>();  // 输出: Value: hello
  m3.print_specific<bool>();         // 输出: Value: 1

  return 0;
}

新增标准库工具

std::optional

#include <iostream>
#include <optional>

int main() {
  auto print = [](auto optional) {
    if (optional) {
      std::cout << "optional contains value: " << *optional << std::endl;
    } else {
      std::cout << "optional does not contain a value" << std::endl;
    }
  };

  std::optional<int> optional;  // 空的 optional
  print(optional);

  optional = 42;
  print(optional);

  optional.reset();  // 清空 optional
  print(optional);

  return 0;
}

关于std::optional, std::variantstd::any的更多信息, 可以参考: 现代 C++ 必备知识: 解锁 std::optional, std::variant 和 std::any


std::variant

std::variant<int, std::string> var;
auto print = [](auto&& arg) {};
var = 42;
std::cout << "var contains an int: " << std::get<int>(var) << std::endl;

var = "hello";
std::cout << "var contains a string: " << std::get<std::string>(var)
          << std::endl;

// 使用 std::holds_alternative 检查当前存储的类型
if (std::holds_alternative<int>(var)) {
  std::cout << "var is an int" << std::endl;
} else if (std::holds_alternative<std::string>(var)) {
  std::cout << "var is a string" << std::endl;
}

关于std::optional, std::variantstd::any的更多信息, 可以参考: 现代 C++ 必备知识: 解锁 std::optional, std::variant 和 std::any


std::any

std::any anyVal;

anyVal = 42;
std::cout << "anyVal contains an int: " << std::any_cast<int>(anyVal)
          << std::endl;

anyVal = "hello";
std::cout << "anyVal contains a string: "
          << std::any_cast<const char*>(anyVal) << std::endl;

// 使用 type() 检查当前存储的类型
if (anyVal.type() == typeid(int)) {
  std::cout << "anyVal is an int" << std::endl;
} else if (anyVal.type() == typeid(std::string)) {
  std::cout << "anyVal is a string" << std::endl;
}

关于std::optional, std::variantstd::any的更多信息, 可以参考: 现代 C++ 必备知识: 解锁 std::optional, std::variant 和 std::any


std::byte

std::byte b1{0x1F};    // 初始化为 0x1F
std::byte b2{0b1010};  // 使用二进制初始化

// 使用 std::to_integer 转换为整数
int intVal = std::to_integer<int>(b1);
std::cout << "b1 as int: " << intVal << std::endl;

// 使用位运算
std::byte b3 = b1 & b2;
std::cout << "b1 & b2: " << std::to_integer<int>(b3) << std::endl;

// 使用 |= 运算符
b1 |= b2;
std::cout << "b1 |= b2: " << std::to_integer<int>(b1) << std::endl;

string views

#include <iostream>
#include <string_view>

void print(std::string_view sv) {
  std::cout << "string view: " << sv << std::endl;
}

int main() {
  std::string normalStr = "This is a test";
  print(normalStr);

  const char* cStr = "Another test";
  print(cStr);

  return 0;
}

进一步阅读: 深入理解 C++ std::string_view — 高效字符串操作的利器


filesystem

// 创建一个目录
fs::path dirPath = "example_dir";
if (!fs::exists(dirPath)) {
  fs::create_directory(dirPath);
  std::cout << "Directory created: " << dirPath << std::endl;
} else {
  std::cout << "Directory already exists: " << dirPath << std::endl;
}

// 创建一个文件并写入内容
fs::path filePath = dirPath / "example.txt";
// 文件属性
fs::file_status status = fs::status(filePath);
std::cout << "File exists: " << fs::exists(status) << std::endl;
std::cout << "Is regular file: " << fs::is_regular_file(status) << std::endl;
std::cout << "File size: " << fs::file_size(filePath) << " bytes"
          << std::endl;

// 检查文件是否存在
if (fs::exists(filePath)) {
  std::cout << "File exists: " << filePath << std::endl;
} else {
  std::cout << "File does not exist: " << filePath << std::endl;
}

关于filesystem库的更多信息, 可以参考: C++17 文件系统库

类型系统(type traits) 扩展

C++17 新增了_v后缀的类型特征,如:

std::is_const_v<T> // 等价于 std::is_const<T>::value

关于 type traits 的更多内容, 请参考: [理解 C++ Type Traits]/posts/understand-cpp-type-traits/

parallel STL algorithms

给 STL 新增了执行策略.

#include <algorithm>
#include <execution>
#include <iostream>
#include <vector>

int main() {
  // 创建一个包含一些整数的向量
  std::vector<int> numbers = {5, 6, 7, 8, 9, 10, 1, 2, 3, 4};

  // 使用并行版本的 std::for_each 对向量中的每个元素进行处理
  std::sort(std::execution::par, numbers.begin(), numbers.end());

  // 输出处理后的向量
  std::cout << "Processed numbers: ";
  for (const auto& num : numbers) {
    std::cout << num << " ";
  }
  std::cout << std::endl;

  return 0;
}

关于 STL 算法执行策略的更多信息, 请参考: C++17 并行计算利器: 深入理解 std::execution 及其性能优化`

子串和子序列的搜索算法

C++17 引入了 Boyer-Moore and Boyer-Moore-Horspool 搜索算法.

std::string text = "ABAAABCD";
std::string pattern = "ABC";

// default_searcher
auto p0 = std::search(text.begin(), text.end(),
                      std::default_searcher{pattern.begin(), pattern.end()});

// boyer_moore_searcher
auto p1 = std::search(text.begin(), text.end(),
             std::boyer_moore_searcher{pattern.begin(), pattern.end()});

// boyer_moore_horspool_searcher
auto p2 = std::search(text.begin(), text.end(),
             std::boyer_moore_horspool_searcher{pattern.begin(), pattern.end()});

other utilities

多线程和并发

删除的特性

Tags: