一篇文章掌握C++17核心特性


语言特性

折叠表达式

可变参数模板编程中我们通常会面临一个问题,即参数包不能直接展开,需要通过一些特殊的方法来实现,因此在一定程度上提高了使用的复杂度。

在 C++ 17 中引入了折叠表达式,其以二元运算符对形参包进行规约,简化了对参数包的展开。

折叠表达式的实例化按以下方式展开成表达式 e

  1. 一元右折叠 (E 运算符 ...) 成为 (E1 运算符 (... 运算符 (EN-1 运算符 EN)))

  2. 一元左折叠 (... 运算符 E) 成为 (((E1 运算符 E2) 运算符 ...) 运算符 EN)

  3. 二元右折叠 (E 运算符 ... 运算符 I) 成为 (E1 运算符 (... 运算符 (EN−1 运算符 (EN 运算符 I))))

  4. 二元左折叠 (I 运算符 ... 运算符 E) 成为 ((((I 运算符 E1) 运算符 E2) 运算符 ...) 运算符 EN)

(其中 N 是包展开中的元素数量)

这里举一个一元左折叠的例子:

template<typename... Args>
bool all(Args... args) { return (... && args); }
 
bool b = all(true, true, true, false);

// 展开结果为 return ((true && true) && true) && false;


类模板实参推导

为了实例化一个类模板,需要知晓但不需要指定每个模板实参。在下列语境中,编译器会从初始化器的类型推导缺失的模板实参:

  • 任意指定变量及变量模板初始化的声明:
std::pair p(2, 4.5);     // 推导出 std::pair<int, double> p(2, 4.5);
std::tuple t(4, 3, 2.5); // 同 auto t = std::make_tuple(4, 3, 2.5);
std::less l;             // 同 std::less<void> l;
  • new 表达式:
template<class T> struct A
{
    A(T, T);
};
auto y = new A{1, 2}; // 分配的类型是 A<int>
  • 函数式转型表达式:
auto lck = std::lock_guard(mtx);     // 推导出 std::lock_guard<std::mutex>
std::copy_n(vi1, 3,
    std::back_insert_iterator(vi2)); // 推导出 std::back_insert_iterator<T>,
                                     // 其中 T 是容器 vi2 的类型
std::for_each(vi.begin(), vi.end(),
    Foo([&](int i) {...}));          // 推导出 Foo<T>,其中 T 是独有的 lambda 类型


结构化绑定

结构化绑定即绑定指定名称到初始化器的子对象或元素。类似引用,结构化绑定是既存对象的别名。不同于引用的是,结构化绑定的类型不必为引用类型。

那么它有什么作用呢?这里我举一个场景,假设我们需要在一个函数中返回多种不同类型的值,在之前的版本主要有以下三种写法:

  • 定义一个结构体,以结构体返回。
  • tuple 返回,通过 std::get() 访问 tuple
  • tuple 返回,通过 std::tie() 解包 tuple
struct Info{
	int x;
	double y;
	string z;
};

Info getInfoByStruct()
{
	return Info{ 1, 2.000, "hello" };
}

tuple<int, double, string> getInfoByTuple()
{
	return make_tuple(2, 3.0000, "world");
}

int main()
{
	//写法一:定义结构体
	Info info = getInfoByStruct();
	cout << info.x << " " << info.y << " " << info.z << endl;
	
	//写法二:返回一个tuple,通过get访问
	tuple<int, double, string> res = getInfoByTuple();
	cout << get<0>(res) << " " << get<1>(res) << " " << get<2>(res) << endl;

	//写法三:返回一个tuple,通过tie解包tuple
	int x;
	double y;
	string z;
	tie(x, y, z) = getInfoByTuple();
	cout << x << " " << y << " " << z << endl;
}
  • 对于方法一来说,如果这个类型的返回值不是很常用,那么定义一个结构体不仅麻烦,而且如果这种情况非常多的时候,大量的结构体很容易引起阅读者的混乱。
  • 对于方法二,通过 get<>() 使用下标的方式访问 tuple 可读性也不高,很难第一时间看出对应的成员是上面,同时也存在越界的风险。
  • 对于方法三,比起上面两种大大提升了代码的可读性,但是仍然存在一个问题,就是需要提前定义好对应的成员变量,然后再通过 tietuple 成员解包到对应变量中,使用起来还是有点麻烦。

为了使上面这种代码简洁、可读性高,在 C++ 17 中就引入了结构化绑定,使用方式如下:

int main()
{
	auto [x, y, z] = getInfoByTuple();
	cout << x << " " << y << " " << z << endl;
}

此时我们可以看见,代码的简洁度、可读性都大大提高了。


inline 变量

在老版本的 C++ 中,如果我们需要定义一个全局变量,则首先要将变量定义在 cpp 文件中,然后在通过 extern 关键字来告诉编译器这个变量已经在其他地方定义过了。

C++ 17 有了内联变量,就可以完美的解决这个问题。我们可以直接将全局变量定义在头文件中,而不用担心出现重定义。

//头文件

class Test {
public:
    inline static const int value = 4;
};

//或者

class Test {
public:
    static const int value; 
};
inline int const A::value = 4;


if 和 switch 语句中的初始化器

在 C++ 17 中支持在 ifswitch 中使用初始化语句,此时代码就可以写成这样:

if (bool res = find(key); res == true)
{
	//执行查找正确逻辑
}

switch (int score = get_score(); score %= 10)
{
    case 1: /*...*/ break;
    case 2: /*...*/ break;
    case 3: /*...*/ break;
    case 4: /*...*/ break;
    case 5: /*...*/ break;
    case 6: /*...*/ break;
}

此时就能够更好的去约束一些临时变量的作用域,同时也能使得代码更加简洁。


库特性

apply

template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t);

apply 以参数的元组调用可调用(Callable)对象 f 。(简单点说就是展开元组,使其作为函数的参数)

元组不必是 std::tuple ,可以为任何支持 std::get 和 std::tuple_size 的类型所替代;特别是可以用 std::array 和 std::pair 。

例如:

int add(int first, int second) { return first + second; }
 
template<typename T>
T add_generic(T first, T second) { return first + second; }
 
auto add_lambda = [](auto first, auto second) { return first + second; };
 
template<typename... Ts>
std::ostream& operator<<(std::ostream& os, std::tuple<Ts...> const& theTuple)
{
    std::apply
    (
        [&os](Ts const&... tupleArgs)
        {
            os << '[';
            std::size_t n{0};
            ((os << tupleArgs << (++n != sizeof...(Ts) ? ", " : "")), ...);
            os << ']';
        }, theTuple
    );
    return os;
}
 
int main()
{
    // OK
    std::cout << std::apply(add, std::pair(1, 2)) << '\n';
 
    // 错误:无法推导函数类型
    // std::cout << std::apply(add_generic, std::make_pair(2.0f, 3.0f)) << '\n'; 
 
    // OK
    std::cout << std::apply(add_lambda, std::pair(2.0f, 3.0f)) << '\n'; 
 
    // 进阶示例
    std::tuple myTuple(25, "Hello", 9.31f, 'c');
    std::cout << myTuple << '\n';
}


make_from_tuple

template <class T, class Tuple>
constexpr T make_from_tuple(Tuple&& t);

构造 T 类型对象,以元组 t 的元素为构造函数的参数。

用法如下:

struct Foo {
    Foo(int first, float second, int third) {
        std::cout << first << ", " << second << ", " << third << "\n";
    }
};
 
int main()
{
   auto tuple = std::make_tuple(42, 3.14f, 0);
   std::make_from_tuple<Foo>(std::move(tuple));
}


any

any 可保有任何可复制构造 (CopyConstructible) 类型的实例的对象。

其主要使用场景如下:

  1. 替代万能类型(如传统的 void*),并提供更好的类型安全和效率。
  2. 避免小对象的动态内存分配。

使用示例如下:

int main()
{
    std::cout << std::boolalpha;
 
    // any 类型
    std::any a = 1;
    std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';
    a = 3.14;
    std::cout << a.type().name() << ": " << std::any_cast<double>(a) << '\n';
    a = true;
    std::cout << a.type().name() << ": " << std::any_cast<bool>(a) << '\n';
 
    // 有误的转型
    try
    {
        a = 1;
        std::cout << std::any_cast<float>(a) << '\n';
    }
    catch (const std::bad_any_cast& e)
    {
        std::cout << e.what() << '\n';
    }
 
    // 拥有值
    a = 1;
    if (a.has_value())
    {
        std::cout << a.type().name() << '\n';
    }
 
    // 重置
    a.reset();
    if (!a.has_value())
    {
        std::cout << "no value\n";
    }
 
    // 指向所含数据的指针
    a = 1;
    int* i = std::any_cast<int>(&a);
    std::cout << *i << "\n";
}


optional

在 C++ 中经常会有一个让人苦恼的问题,当我们需要在某个结构中查询一个对象,并将其作为返回值时,如果查询不到,则难以去表述查询失败,例如下面的代码:

template<typename K, typename V>
class TestMap
{
public:
	V get(const K& key)
	{
		if (_set.count(key) == 0)
		{
            //难以表述查询失败,由于不是指针类型,所以无法返回nullptr,只能约定一个空值
			return V();
		}
		return _set[key];
	}

prvate:
	unordered_map<K, V> _set;
};

由于其不是指针类型,所以无法返回 nullptr,只能提前约定一个值作为错误结果。

在这种情况下,通常我们会采用以下两种方法来解决:

  • 返回一个 V*,但是此时又会存在内存管理的问题,因此需要使用智能指针。
  • 返回一个 pair(V, bool),也是最常用的方法,但同时会存在构造对象的成本。

上面的两种方法都较为麻烦,因此在 C++ 14 中引入 optional 来完美的解决这个问题。

optional 管理一个可选的容纳值,既可以存在也可以不存在的值。与其他手段,如 std::pair<T,bool> 相比, optional 良好地处理构造开销高昂的对象,并更加可读。

这里我们改造一下上面的代码

template<typename K, typename V>
class TestMap
{
public:
	optional<V> get(const K& key)
	{
		if (_set.find(key) == _set.end())
		{
			return nullopt;
		}
		return _set[key];
	}
private
	unordered_map<K, V> _set;
};


file_system

在老版本的 C++ 中,并没有封装对文件系统的操作,因此我们通常需要调用一些第三方库(如 boost)或者操作系统提供的 api 来实现对应的功能。

在 C++ 17 中,正式将 file_system 加入进标准库,其定义的成员与函数如下:

#include <compare>
 
namespace std::filesystem {
  // 路径
  class path;
 
  // 路径非成员函数
  void swap(path& lhs, path& rhs) noexcept;
  size_t hash_value(const path& p) noexcept;
 
  // 文件系统错误
  class filesystem_error;
 
  // 目录条目
  class directory_entry;
 
  // 目录迭代器
  class directory_iterator;
 
  // 目录迭代器的范围访问
  directory_iterator begin(directory_iterator iter) noexcept;
  directory_iterator end(directory_iterator) noexcept;
 
  // 递归目录迭代器
  class recursive_directory_iterator;
 
  // 递归目录迭代器的范围访问
  recursive_directory_iterator begin(recursive_directory_iterator iter) noexcept;
  recursive_directory_iterator end(recursive_directory_iterator) noexcept;
 
  // 文件状况
  class file_status;
 
  struct space_info {
    uintmax_t capacity;
    uintmax_t free;
    uintmax_t available;
 
    friend bool operator==(const space_info&, const space_info&) = default;
  };
 
  // 枚举
  enum class file_type;
  enum class perms;
  enum class perm_options;
  enum class copy_options;
  enum class directory_options;
 
  using file_time_type = chrono::time_point<chrono::file_clock>;
 
  // 文件系统操作
  path absolute(const path& p);
  path absolute(const path& p, error_code& ec);
 
  path canonical(const path& p);
  path canonical(const path& p, error_code& ec);
 
  void copy(const path& from, const path& to);
  void copy(const path& from, const path& to, error_code& ec);
  void copy(const path& from, const path& to, copy_options options);
  void copy(const path& from, const path& to, copy_options options,
            error_code& ec);
 
  bool copy_file(const path& from, const path& to);
  bool copy_file(const path& from, const path& to, error_code& ec);
  bool copy_file(const path& from, const path& to, copy_options option);
  bool copy_file(const path& from, const path& to, copy_options option,
                 error_code& ec);
 
  void copy_symlink(const path& existing_symlink, const path& new_symlink);
  void copy_symlink(const path& existing_symlink, const path& new_symlink,
                    error_code& ec) noexcept;
 
  bool create_directories(const path& p);
  bool create_directories(const path& p, error_code& ec);
 
  bool create_directory(const path& p);
  bool create_directory(const path& p, error_code& ec) noexcept;
 
  bool create_directory(const path& p, const path& attributes);
  bool create_directory(const path& p, const path& attributes,
                        error_code& ec) noexcept;
 
  void create_directory_symlink(const path& to, const path& new_symlink);
  void create_directory_symlink(const path& to, const path& new_symlink,
                                error_code& ec) noexcept;
 
  void create_hard_link(const path& to, const path& new_hard_link);
  void create_hard_link(const path& to, const path& new_hard_link,
                        error_code& ec) noexcept;
 
  void create_symlink(const path& to, const path& new_symlink);
  void create_symlink(const path& to, const path& new_symlink,
                      error_code& ec) noexcept;
 
  path current_path();
  path current_path(error_code& ec);
  void current_path(const path& p);
  void current_path(const path& p, error_code& ec) noexcept;
 
  bool equivalent(const path& p1, const path& p2);
  bool equivalent(const path& p1, const path& p2, error_code& ec) noexcept;
 
  bool exists(file_status s) noexcept;
  bool exists(const path& p);
  bool exists(const path& p, error_code& ec) noexcept;
 
  uintmax_t file_size(const path& p);
  uintmax_t file_size(const path& p, error_code& ec) noexcept;
 
  uintmax_t hard_link_count(const path& p);
  uintmax_t hard_link_count(const path& p, error_code& ec) noexcept;
 
  bool is_block_file(file_status s) noexcept;
  bool is_block_file(const path& p);
  bool is_block_file(const path& p, error_code& ec) noexcept;
 
  bool is_character_file(file_status s) noexcept;
  bool is_character_file(const path& p);
  bool is_character_file(const path& p, error_code& ec) noexcept;
 
  bool is_directory(file_status s) noexcept;
  bool is_directory(const path& p);
  bool is_directory(const path& p, error_code& ec) noexcept;
 
  bool is_empty(const path& p);
  bool is_empty(const path& p, error_code& ec);
 
  bool is_fifo(file_status s) noexcept;
  bool is_fifo(const path& p);
  bool is_fifo(const path& p, error_code& ec) noexcept;
 
  bool is_other(file_status s) noexcept;
  bool is_other(const path& p);
  bool is_other(const path& p, error_code& ec) noexcept;
 
  bool is_regular_file(file_status s) noexcept;
  bool is_regular_file(const path& p);
  bool is_regular_file(const path& p, error_code& ec) noexcept;
 
  bool is_socket(file_status s) noexcept;
  bool is_socket(const path& p);
  bool is_socket(const path& p, error_code& ec) noexcept;
 
  bool is_symlink(file_status s) noexcept;
  bool is_symlink(const path& p);
  bool is_symlink(const path& p, error_code& ec) noexcept;
 
  file_time_type last_write_time(const path& p);
  file_time_type last_write_time(const path& p, error_code& ec) noexcept;
  void last_write_time(const path& p, file_time_type new_time);
  void last_write_time(const path& p, file_time_type new_time,
                       error_code& ec) noexcept;
 
  void permissions(const path& p, perms prms, perm_options opts=perm_options::replace);
  void permissions(const path& p, perms prms, error_code& ec) noexcept;
  void permissions(const path& p, perms prms, perm_options opts, error_code& ec);
 
  path proximate(const path& p, error_code& ec);
  path proximate(const path& p, const path& base = current_path());
  path proximate(const path& p, const path& base, error_code& ec);
 
  path read_symlink(const path& p);
  path read_symlink(const path& p, error_code& ec);
 
  path relative(const path& p, error_code& ec);
  path relative(const path& p, const path& base = current_path());
  path relative(const path& p, const path& base, error_code& ec);
 
  bool remove(const path& p);
  bool remove(const path& p, error_code& ec) noexcept;
 
  uintmax_t remove_all(const path& p);
  uintmax_t remove_all(const path& p, error_code& ec);
 
  void rename(const path& from, const path& to);
  void rename(const path& from, const path& to, error_code& ec) noexcept;
 
  void resize_file(const path& p, uintmax_t size);
  void resize_file(const path& p, uintmax_t size, error_code& ec) noexcept;
 
  space_info space(const path& p);
  space_info space(const path& p, error_code& ec) noexcept;
 
  file_status status(const path& p);
  file_status status(const path& p, error_code& ec) noexcept;
 
  bool status_known(file_status s) noexcept;
 
  file_status symlink_status(const path& p);
  file_status symlink_status(const path& p, error_code& ec) noexcept;
 
  path temp_directory_path();
  path temp_directory_path(error_code& ec);
 
  path weakly_canonical(const path& p);
  path weakly_canonical(const path& p, error_code& ec);
}
 
// 散列支持
namespace std {
  template<class T> struct hash;
  template<> struct hash<filesystem::path>;
}
 
namespace std::ranges {
  template<>
  inline constexpr bool enable_borrowed_range<filesystem::directory_iterator> = true;
  template<>
  inline constexpr bool
    enable_borrowed_range<filesystem::recursive_directory_iterator> = true;
 
  template<>
  inline constexpr bool enable_view<filesystem::directory_iterator> = true;
  template<>
  inline constexpr bool enable_view<filesystem::recursive_directory_iterator> = true;
}


variant

variant 表示一个类型安全的联合体,即加强版的 union。其在 union 的基础上支持更多的复杂数据类型如 stringmap 等。

int main()
{
    std::variant<int, float> v, w;
    v = 12; // v 含 int
    int i = std::get<int>(v);
    w = std::get<int>(v);
    w = std::get<0>(v); // 与前一行效果相同
    w = v; // 与前一行效果相同
 
//  std::get<double>(v); // 错误: [int, float] 中无 double
//  std::get<3>(v);      // 错误:合法下标值为 0 与 1
 
    try {
      std::get<float>(w); // w 含 int 而非 float :将抛出
    }
    catch (const std::bad_variant_access&) {}
 
    using namespace std::literals;
 
    std::variant<std::string> x("abc"); // 转换构造函数在无歧义时起作用
    x = "def"; // 转换赋值在无歧义时亦起作用
 
    std::variant<std::string, void const*> y("abc");
    // 传递 char const * 时转换成 void const *
    assert(std::holds_alternative<void const*>(y)); // 成功
    y = "xyz"s;
    assert(std::holds_alternative<std::string>(y)); // 成功
}

但其仍然不支持存储引用、数组,或空类型 void 、空 variant (可用 std::variant<std::monostate> 代替)。(由于 union 所有成员共享同一块空间,一些类型难以实现)


string_view

string_view 是一个只读的字符串视图,通常用于字符串传参。其最大的特点就是能够避免传递字符串时产生的不必要的拷贝以及函数调用。

用法如下:

void print(string_view str)
{
	cout << str << endl;
}

int main()
{
	string str("hello world");

	print(string_view(str.c_str(), str.size()));
}

为什么我们不使用 const string& 或者 const char* 而是使用 string_view 呢?

首先以 const string& 为例,如果实参不是 string 类型,此时就会引发不必要的内存分配和拷贝,从而影响性能。而对于 const char*,可能多引发一次类似 strlen 的调用。

但如果我们维护的是 C class 的老代码,那此时就不太适合使用 string_view 了,因为其并不保证 /0 结尾,因此可能存在潜在的风险。同时其只是一个视图,当源字符串生命周期结束后, string_view 自然也无法使用。


byte

在老版本中,我们想要存储一个字节时,通常会使用 bool 或者 char 类型。由于使用这些类型可能会存在一些潜在的风险,因此在 C++ 17 中引入了 byte 类型,它实现指定于 C++ 语言定义中的字节的概念。

char 不同的是,它不是字符类型且非算术类型。 byte 只是位的汇集,而且只对它定义逐位运算符。(可以用于实现 bitmap、bloomfilter 等结构)

int main()
{
    std::byte b{42};
    std::cout << std::to_integer<int>(b) << "\n";
 
    // b *= 2 编译错误
    b <<= 1;
    std::cout << std::to_integer<int>(b) << "\n";
}


STL 并行算法 与 execution

C++ 17 中的核心就是重载了大多数原有的 STL 算法(重载 69 个,新增 7 个),使其支持多种执行策略。(原本只有串行)并通过策略模式抽象出 execution 策略类,我们只需要传递一个 execution 对象就可以决定四种执行策略:

  • sequenced_policy(顺序执行策略)
  • parallel_policy(并行执行策略)
  • parallel_unsequenced_policy(并行及无序执行策略)
  • unsequenced_policy(无序执行策略)
namespace std {
  // 执行策略类型特征
  template<class T> struct is_execution_policy;
  template<class T>
  inline constexpr bool is_execution_policy_v = is_execution_policy<T>::value;
}
 
namespace std::execution {
  // 顺序执行策略
  class sequenced_policy;
 
  // 并行执行策略
  class parallel_policy;
 
  // 并行及无序执行策略
  class parallel_unsequenced_policy;
 
  // 无序执行策略
  class unsequenced_policy;
 
  // 执行策略对象
  inline constexpr sequenced_policy            seq{ /* 未指明 */ };
  inline constexpr parallel_policy             par{ /* 未指明 */ };
  inline constexpr parallel_unsequenced_policy par_unseq{ /* 未指明 */ };
  inline constexpr unsequenced_policy          unseq{ /* 未指明 */ };
}

这里就简单的测试一下它们的性能。以 sort 为例子,首先生成 500 万个 范围从 0 ~ 100 万的随机数,然后分别使用串行和并行两种策略进行计算,看看性能差别有多大

int main()
{
	std::vector<int> numbers(1000000);
	std::random_device rd;
	std::mt19937 gen(rd());
	std::uniform_int_distribution<int> dis(0, 1000000);

	auto rand_num([&dis, &gen]() mutable { return dis(gen); });

	std::generate(std::execution::seq, std::begin(numbers), std::end(numbers),
		rand_num);

	auto start = std::chrono::system_clock::now();

	sort(execution::seq, numbers.begin(), numbers.end());

	auto end = std::chrono::system_clock::now();
	auto duration = duration_cast<microseconds>(end - start);

	cout << "串行计算时间:"
		<< double(duration.count()) * microseconds::period::num / microseconds::period::den
		<< "秒" << endl;

	start = std::chrono::system_clock::now();
	sort(execution::par, numbers.begin(), numbers.end());
	end = std::chrono::system_clock::now();
	duration = duration_cast<microseconds>(end - start);
	cout << "并行计算时间:"
		<< double(duration.count()) * microseconds::period::num / microseconds::period::den
		<< "秒" << endl;
}

输出结果如下

串行计算时间:11.4151秒
并行计算时间:1.92892秒

如果想了解更详细的并行算法内容,可以看看下面这篇文章 C++17: New Parallel Algorithms of the Standard Template Library

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凌桓丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值