快速入门C++17:了解最新的语言特性和功能(下)

简介: 快速入门C++17:了解最新的语言特性和功能

二、C++17经常考到的知识点

2.1C++17中对类型推断的改进,以及使用auto关键字进行自动类型推导


在C++17中,对类型推断进行了一些改进和扩展。其中一个主要的改进是通过使用auto关键字进行自动类型推导。


在之前的C++版本中,我们可以使用auto关键字来声明变量,并让编译器自动推导其类型。但是,在C++17中,我们可以将auto关键字用于函数返回值、函数参数以及非静态数据成员的声明上。


具体而言,C++17引入了以下改进:


  1. 自动函数返回值推导:
    auto functionName(parameters) -> return_type { ... }
    在函数定义中,可以使用auto作为返回类型,并使用箭头符号(->)指定具体的返回类型。编译器会根据函数体内的表达式来推导出准确的返回类型。
  2. 自动函数参数类型推导:
    template  void functionName(T parameter) { ... }
    函数模板和普通函数可以使用自动参数类型推导。在调用时,编译器会根据实际传入的参数来推断模板或者函数参数的具体类型。
  3. auto用于非静态数据成员声明:
    class ClassName { auto memberVariable = value; ... };


在类定义中,我们可以使用auto关键字来声明非静态数据成员,并让编译器根据初始值进行类型推断。


这些改进使得代码更加简洁,减少了冗余的类型声明,并提高了代码的可读性和可维护性。但是需要注意的是,过度使用auto可能会降低代码的可读性,所以在使用时需要权衡利弊。


2.2如何使用结构化绑定来方便地从元组或其他数据结构中提取数据


结构化绑定是C++17引入的一个特性,可以方便地从元组或其他数据结构中提取数据。以下是使用结构化绑定的示例:


#include <tuple>
#include <iostream>
int main() {
    std::tuple<int, std::string, double> data{ 42, "Hello", 3.14 };
    // 使用结构化绑定从元组中提取数据
    auto [value1, value2, value3] = data;
    // 输出提取的值
    std::cout << "Value 1: " << value1 << std::endl;
    std::cout << "Value 2: " << value2 << std::endl;
    std::cout << "Value 3: " << value3 << std::endl;
    return 0;
}


在上面的代码中,我们创建了一个包含整数、字符串和浮点数的元组data。然后,使用结构化绑定将元组中的值分别赋给value1value2value3变量。最后,我们打印输出了这些值。


除了元组,你还可以将结构化绑定应用于其他支持结构化访问的数据结构,例如数组、自定义类型等。


2.3if constexpr语句,它允许在编译时根据条件进行条件编译


C++17引入了if constexpr语句,它允许在编译时根据条件进行条件编译。通过使用if constexpr语句,可以在编译时选择性地执行代码块。如果条件在编译时求值为真,则执行if分支;如果条件在编译时求值为假,则跳过if分支。这种方式可以优化程序的性能,并提供更灵活的控制流程。


2.4折叠表达式的用法,它可以简化对多个变量展开操作的代码


折叠表达式是C++17引入的一种语法,用于在编译期对多个变量进行展开操作,从而简化代码。它通过使用逗号运算符和展开参数包(parameter pack)来实现。


下面是一个示例,展示了如何使用折叠表达式来计算多个变量的求和:

#include <iostream>
template<typename... Args>
auto sum(Args... args) {
    return (args + ...);
}
int main() {
    int result = sum(1, 2, 3, 4, 5);
    std::cout << "Sum: " << result << std::endl;
    return 0;
}


在这个例子中,sum 函数使用折叠表达式 (args + ...) 对参数包 args 进行展开,并计算它们的求和。函数模板 sum 的参数列表中使用了模板参数包 typename... Args,以便接收任意数量的参数。


运行上述代码会输出结果 Sum: 15,表示对传入的整数值进行了求和操作。


通过使用折叠表达式,我们可以简化对多个变量展开操作的代码,使得代码更加简洁和可读。需要注意的是,折叠表达式仅在 C++17 及以上版本中可用。


2.5constexpr函数的概念和用法,它允许在编译时计算出结果并将其用于编译期间的操作


constexpr函数是在编译时期计算结果并将其用于编译期间操作的函数。它可以在编译时进行常量表达式求值,这意味着可以使用它来执行一些简单的计算或生成常量值。


以下是一些constexpr函数的概念和用法:

  1. 概念:constexpr函数必须满足一些要求,例如其参数和返回值类型必须是字面类型(Literal Type),函数体中只能包含能被编译器求值的语句,且不能包含控制流语句(如循环、条件分支等)。
  2. 编译时计算:通过将参数传递给constexpr函数,在编译过程中就可以进行常量表达式求值,并将结果用于其他编译期间的操作,比如数组大小、模板实例化等。
  3. 常量折叠:当多个constexpr函数相互调用时,它们会在编译期间展开,并以常量方式计算出最终结果。


下面是一个示例代码:


constexpr int add(int a, int b) {
    return a + b;
}
int main() {
    constexpr int result = add(5, 7); // 在编译期间计算结果为12
    static_assert(result == 12, "Result should be 12");
    constexpr int size = add(3, add(4, 5)); // 常量折叠,编译期间计算结果为12
    static_assert(size == 12, "Size should be 12");
    return 0;
}


在这个例子中,add()函数是一个constexpr函数,它可以在编译时对参数进行求值并返回结果。通过使用constexpr修饰变量,在编译时检查结果是否正确。


需要注意的是,constexpr函数的用法有一些限制,例如不能包含动态内存分配、I/O操作等。但它可以提供一种在编译期间进行简单计算和优化的方式。


2.6C++17中引入的并行算法库,可以方便地实现并行计算任务


C++17引入了一个名为的头文件中的并行算法库,它提供了一系列可以方便地实现并行计算任务的算法。这些算法允许将任务分解成多个子任务,并在多个线程上同时执行,从而提高程序的性能。


这些并行算法使用了底层的线程库(如std::thread)来实现并发执行。常用的一些并行算法包括std::for_each、std::transform、std::reduce等,它们都有对应的并行版本std::for_each_par、std::transform_par、std::reduce_par等。


使用并行算法时,需要注意数据之间是否存在数据竞争或其他线程安全问题。确保共享数据被正确地保护起来,以避免潜在的错误。


需要注意的是,并行算法库只能在支持多线程的环境中发挥作用,在单核处理器上运行时,并行算法会退化为串行算法。因此,在选择使用并行算法时需要考虑到系统资源和具体场景。

2.7C++17中引入的文件系统库,提供了更方便、更高级别的文件和目录操作接口


是的,C++17引入了文件系统库(std::filesystem),提供了更方便和高级别的文件和目录操作接口。它通过一组类和函数来处理路径、文件和目录的创建、删除、重命名、遍历等操作,大大简化了文件系统操作的编程任务。使用文件系统库,你可以更轻松地进行文件和目录的管理,并且跨平台兼容性良好。


三、C++17新特性总结


C ++ 17是标准C ++语言的最新版本。C ++ 11和C ++ 14是C ++的早期版本。当前版本对核心语言做了一些补充,同时还删除了一些以前的功能。C ++ 17被称为功能完整或功能完整。


最近很火的鸿蒙系统框架代码,很多就是基于c++做的底层、运行时和应用框架封装。c++11后续又有11,14,17,20等众多新版本。哪个是主流?截至目前来说,c++17应该是新项目的首选。C++14在11的基础上查缺补漏,并未加入许多新特性,而C++17作为C++11后的第一个大版本,标志着现代C++逐渐走向成熟。


C ++ 17中引入了一些新更改-


库更改-utils


这是C ++ 17最惊人的功能之一。它合并了其他库的功能和模式。许多子库都合并到了标准中。


以下功能已添加到C ++ 17的utils库中-

  • std::variant
  • std::search
  • std::string_view
  • std ::可选
  • std::any


库更改-文件系统


以前的标准库缺少一些关键功能。C ++委员会加强了库,并将许多系统合并为标准。


以下功能已添加到C ++ 17的文件系统中-

  • 使用路径对象
  • 编译器/库支持
  • 穿越路径


库更改-并行算法


它为C ++ 11和C ++ 17的先前功能添加了一些新功能。它为标准库解锁了自动并行化或自动矢量化功能。


以下功能已添加到C ++ 17的并行算法中-

  • 算法更新
  • 执行政策
  • 新算法

属性


C ++ 17添加了三个新属性,例如[[maybe_unused]],[[fallthrough]]和[[nodiscard]]。


简化版


有一些功能可以简化C ++早期版本中的代码。C ++ 17在以下功能中添加了此功能,以简化代码-

  • if / switch的初始化语句
  • 内联变量
  • 结构化绑定声明


搜索者

C ++ 17改进了早期版本的C ++语言中的旧搜索器。boyer_moore_horspool_searcher,default_searcher和boyer_moore_searcher是C ++ 17中的新搜索器。


c++17新特性有:


  • 1、对auto表达式推导的规则进行了改变;
  • 2、lambda表达式可以捕获“*this”;
  • 3、新增inline变量,可以直接将全局变量定义在头文件中;
  • 4、条件表达式中支持初始化语句;
  • 5、枚举的直接列表初始化等等。
  • ......


3.1auto关键字


从c++11开始,auto关键字能够通过初始化器推导出变量的类型。在c++14中,auto关键字的能力进一步提升,能够通过return语句推导出函数的返回类型。 使用auto关键字能够提高编码效率,同时能够简化重构流程。但是,C++11中的auto推导,往往结果与预期的不同。


c++11 中为了支持统一初始化,引入了新的统一初始化语法,如下所示:


// c++11
auto x3{ 1, 2 }; // std::initializer_list<int>
auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int>
auto x5{ 3 };    // std::initializer_list<int>


这三种方式初始化的变量,最终类型推导的结果都是 std::initializer_list , 而不是我们认为的int。 这是因为 当用于auto声明变量的表达式是{}括起来的,推导的型别就会变成 std::initializer_list。


在C++17中,对auto表达式推导的规则进行了改变。

// c++17
auto x3{ 1, 2 }; // error: not a single element
auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int>
auto x5{ 3 };    // decltype(x5) is int


对比发现 auto x5{3}, 会直接将变量推导成 x5, 而 x3{1, 2} 这种方式也会编译失败。auto推导的规则变得更加直观。


3.2lambda表达式


lambda也是c++11中引入的,在C++11中,lambda表达式只能用捕获this,this是当前对象的一个只读的引用。 在C++17中,可以捕获*this, *this是当前对象的一个拷贝,捕获当前对象的拷贝,能够确保当前对象释放后, lambda表达式能安全的调用this中的变量和方法。


3.3inline变量


Inline 变量, inline变量可以让变量有多于一次的定义。C++17之前,我们定义全局变量, 总需要将变量定义在cpp文件中,然后在通过extern关键字来告诉编译器 这个变量已经在其他地方定义过了。 inline变量出现后,我们可以直接将全局变量定义在头文件中,而不用担心出现redefine的错误信息。


3.4namespace嵌套


namespace A {
    namespace B {
        namespace C {
            void func();
        }
    }
}
// c++17
namespace A::B::C {
    void func();
}


3.5from_chars函数和to_chars


std::array<char, 3> str{"42"};
int result;
std::from_chars( str.data(), str.data()+str.size(),result );
std::cout << result << std::endl;
// p是填充到str以后的最后一个迭代器
if(auto [p, ec] = std::to_chars(str.data(), str.data() + str.size(), 425);
   ec == std::errc()){
    if(p == str.end()){
        std::cout << "hello world\n";
    }
        std::cout << std::string_view(str.data(), p - str.data());
}
输出:
42
hello world
425


3.6其他好用的新特性


结构化绑定


c++17最便利的语法糖当属结构化绑定。结构化绑定是指将array、tuple或struct的成员绑定到一组变量*上的语法,最常用的场景是在遍历map/unordered_map时不用再声明一个中间变量了:

其他好用的新特性
结构化绑定
c++17最便利的语法糖当属结构化绑定。
结构化绑定是指将array、tuple或struct的成员绑定到一组变量*上的语法,
最常用的场景是在遍历map/unordered_map时不用再声明一个中间变量了:


需要注意的是,结构化绑定的结果并不是变量,c++标准称之为名字/别名,这也导致它们不允许被lambda捕获,但是gcc并没有遵循c++标准,所以以下代码在gcc可以编译,clang则编译不过。

for(const auto& [key, value]: map){
    [&key, &value]{
        std::cout << key << ": " << value << std::endl;
    }();
}


在clang环境下,可以在lambda表达式捕获时显式引入一个引用变量通过编译:

for(const auto& [key, value]: map){
    [&key = key, &value = value]{
        std::cout << key << ": " << value << std::endl;
    }();
}


但是这条限制在c++20中已经被删除,所以在c++20标准中gcc和clang都可以捕获结构化绑定的对象了。上述第一种写法在c++20里都是ok的。


3.7std::tuple的隐式推导


在c++17以前,构造std::pair/std::tuple时必须指定数据类型或使用std::make_pair/std::make_tuple函数,c++17为std::pair/std::tuple新增了推导规则,可以不再显示指定类型。


// pre c++17
std::pair<int, std::string> p1{3.14, "pi"s};
auto p1 = std::make_pair(3.14, "pi"s);
// c++17
std::pair p3{3.14, "pi"s};


if constexpr


if constexpr语句是编译期的if判断语句,在C++17以前做编译期的条件判断往往通过复杂SFINAE机制或模版重载实现,甚至嫌麻烦的时候直接放到运行时用if判断,造成性能损耗,if constexpr大大缓解了这个问题。比如我想实现一个函数将不同类型的输入转化为字符串,在c++17之前需要写三个函数去实现,而c++17只需要一个函数。

// pre c++17
template <typename T>
std::string convert(T input){
    return std::to_string(input);
}
// const char*和string进行特殊处理
std::string convert(const char* input){
    return input;
}
std::string convert(std::string input){
    return input;
}
// c++17
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);
    }
}


if初始化语句


c++17支持在if的判断语句之前增加一个初始化语句,将仅用于if语句内部的变量声明在if内,有助于提升代码的可读性。且对于lock/iterator等涉及并发/RAII的类型更容易保证程序的正确性。

if (auto it = m.find(10); it != m.end()) { return it->second.size(); }
if (char buf[10]; std::fgets(buf, 10, stdin)) { m[0] += buf; }
if (std::lock_guard lock(mx); shared_flag) { unsafe_ping(); shared_flag = false; }


3.8std::shared_mutex


读写锁,性能提升,shared_mutex是c++的原生读写锁实现,有共享和独占两种锁模式,适用于并发高的读场景下,通过reader之前共享锁来提升性能。在c++17之前,只能自己通过独占锁和条件变量自己实现读写锁或使用c++14加入的性能较差的std::shared_timed_mutex。


通过shared_mutex实现的线程安全计数器:

// c++17
class ThreadSafeCounter {
 public:
  ThreadSafeCounter() = default;
  // Multiple threads/readers can read the counter's value at the same time.
  unsigned int get() const {
    std::shared_lock lock(mutex_);
    return value_;
  }
  // Only one thread/writer can increment/write the counter's value.
  unsigned int increment() {
    std::unique_lock lock(mutex_);
    return ++value_;
  }
  // Only one thread/writer can reset/write the counter's value.
  void reset() {
    std::unique_lock lock(mutex_);
    value_ = 0;
  }
 private:
  mutable std::shared_mutex mutex_;
  unsigned int value_ = 0;
};


3.9std::string_view


std::string_view顾名思义是字符串的“视图”,类成员变量包含两个部分:字符串指针和字符串长度,std::string_view涵盖了std::string的所有只读接口。std::string_view对字符串不具有所有权,且兼容std::string和const char*两种类型。c++17之前,我们处理只读字符串往往使用const std::string&,std::string有两点性能优势:


  1. 兼容两种字符串类型,减少类型转换和内存分配。如果传入的是明文字符串const char*, const std::string&需要进行一次内存分配,将字符串拷贝到堆上,而std::string_view则可以避免。
  2. 在处理子串时,std::string::substr也需要进行拷贝和分配内存,而std::string_view::substr则不需要,在处理大文件解析时,性能优势非常明显。


平时代码中可以大规模使用的一个特性。其实对于string的争论一直没有停止过,很多人认为string是字节串而不是字符串,因为string是可以改变的,这一切争论到C++17可以停止了。string_view的substr与构造时间复杂度为O(1),且不会产生拷贝,因为substr只是一个指针操作。


3.10std::file_system


其实模子是boost的file_system,最早2003年就出来了,因为是跨平台的,所以可以说是非常舒服了。

namespace fs = std::filesystem;
fs::path pathToShow("/home/lzl/Desktop/execise");
cout << "exists() = " << fs::exists(pathToShow) << "\n"
<< "root_name() = " << pathToShow.root_name() << "\n"
<< "root_path() = " << pathToShow.root_path() << "\n";


std::map/unordered_map try_emplace


在向std::map/unordered_map中插入元素时,我们往往使用emplace,emplace的操作是如果元素key不存在,则插入该元素,否则不插入。但是在元素已存在时,emplace仍会构造一次待插入的元素,在判断不需要插入后,立即将该元素析构,因此进行了一次多余构造和析构操作。c++17加入了try_emplace,避免了这个问题。同时try_emplace在参数列表中将key和value分开,因此进行原地构造的语法比emplace更加简洁。

std::map<std::string, std::string> m;
// emplace的原地构造需要使用std::piecewise_construct,因为是直接插入std::pair<key, value>
m.emplace(std::piecewise_construct,
           std::forward_as_tuple("c"),
           std::forward_as_tuple(10, 'c'));
// try_emplace可以直接原地构造,因为参数列表中key和value是分开的
m.try_emplace("c", 10, 'c')


同时,c++17还给std::map/unordered_map加入了insert_or_assign函数,可以更方便地实现插入或修改语义。


3.11std::apply


相当于把容器的值当做函数的输入。

int add_ten(int first, int second) { return first + second; }
auto add_ten_lambda = [](auto first, auto second) { return first + second; };
std::cout << std::apply(add_ten, std::pair(1, 2)) << '\n';
//std::cout << add(std::pair(1, 2)) << "\n"; // error
std::cout << std::apply(add_ten_lambda, std::tuple(2.2f, 3.0f)) << '\n';
输出:
3
5.2


3.12类型系统


c++17进一步完备了c++的类型系统,终于加入了众望所归的类型擦除容器(Type Erasure)和代数数据类型(Algebraic Data Type)。


std::any


std::any是一个可以存储任何可拷贝类型的容器,C语言中通常使用void*实现类似的功能,与void*相比,std::any具有两点优势:

  1. std::any更安全:在类型T被转换成void*时,T的类型信息就已经丢失了,在转换回具体类型时程序无法判断当前的void*的类型是否真的是T,容易带来安全隐患。而std::any会存储类型信息,std::any_cast是一个安全的类型转换。
  2. std::any管理了对象的生命周期,在std::any析构时,会将存储的对象析构,而void*则需要手动管理内存。


std::any应当很少是程序员的第一选择,在已知类型的情况下,std::optional, std::variant和继承都是比它更高效、更合理的选择。只有当对类型完全未知的情况下,才应当使用std::any,比如动态类型文本的解析或者业务逻辑的中间层信息传递。


3.13std::optional


std::optional代表一个可能存在的T值,对应Haskell中的Maybe和Rust/OCaml中的option,实际上是一种Sum Type。常用于可能失败的函数的返回值中,比如工厂函数。在C++17之前,往往使用T*作为返回值,如果为nullptr则代表函数失败,否则T*指向了真正的返回值。但是这种写法模糊了所有权,函数的调用方无法确定是否应该接管T*的内存管理,而且T*可能为空的假设,如果忘记检查则会有SegFault的风险。

// pre c++17
ReturnType* func(const std::string& in) {
    ReturnType* ret = new ReturnType;
    if (in.size() == 0)
        return nullptr;
    // ...
    return ret;
}
// c++17 更安全和直观
std::optional<ReturnType> func(const string& in) {
    ReturnType ret;
    if (in.size() == 0)
        return nullopt;
    // ...
    return ret;
}


3.14std::variant


std::variant代表一个多类型的容器,容器中的值是制定类型的一种,是通用的Sum Type,对应Rust的enum。是一种类型安全的union,所以也叫做tagged union。与union相比有两点优势:


  1. 可以存储复杂类型,而union只能直接存储基础的POD类型,对于如std::vector和std::string就等复杂类型则需要用户手动管理内存。
  2. 类型安全,variant存储了内部的类型信息,所以可以进行安全的类型转换,c++17之前往往通过union+enum来实现相同功能。


通过使用std::variant,用户可以实现类似Rust的std::result,即在函数执行成功时返回结果,在失败时返回错误信息,上文的例子则可以改成。

需要注意的是,c++17只提供了一个库级别的variant实现,没有对应的模式匹配(Pattern Matching)机制,而最接近的std::visit又缺少编译器的优化支持,所以在c++17中std::variant并不好用,跟Rust和函数式语言中出神入化的Sum Type还相去甚远,但是已经有许多围绕std::variant的提案被提交给c++委员会探讨,包括模式匹配,std::expected等等。


总结一下,c++17新增的三种类型给c++带来了更现代更安全的类型系统,它们对应的使用场景是:

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


3.14并行算法库


这可以说是C++17最重要的几个特性之一,这个特性为几乎所有标准库函数加上一个执行策略参数,可以让使用者选择并行还是串行,这不仅包括七个新的算法,还有我们熟知的sort等。


其他特性:


除此之外,C++17还增加了一些其他特性,文中没有一一列出。

  • bool 表达式不能用 ++, – 这两个自增(减)运算符了
  • c++17中异常已经成为了类型系统的一部分,
  • 枚举的直接列表初始化
  • 结构化绑定
  • constexpr if 表达式
  • map支持merge和extract
相关文章
|
16天前
|
C语言 C++ 开发者
深入探索C++:特性、代码实践及流程图解析
深入探索C++:特性、代码实践及流程图解析
|
1月前
|
编译器 C++
C++语言预处理器学习应用案例
【4月更文挑战第8天】C++预处理器包括条件编译、宏定义和文件包含等功能。例如,条件编译用于根据平台选择不同代码实现,宏定义可简化常量和变量名,文件包含则用于整合多个源文件。示例中展示了如何使用`#ifdef`等指令进行条件编译,当`DEBUG`宏定义时,`PRINT_LOG`会打印调试信息,否则不执行。
14 1
|
2天前
|
自然语言处理 编译器 C语言
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
本文章是我对C++学习的开始,很荣幸与大家一同进步。 首先我先介绍一下C++,C++是上个世纪为了解决软件危机所创立 的一项面向对象的编程语言(OOP思想)。
26 1
|
3天前
|
算法 编译器 C++
【C++入门到精通】新的类功能 | 可变参数模板 C++11 [ C++入门 ]
【C++入门到精通】新的类功能 | 可变参数模板 C++11 [ C++入门 ]
20 1
|
11天前
|
Linux 程序员 图形学
C++语言在现代软件开发中的应用与实践
C++语言在现代软件开发中的应用与实践
19 2
|
12天前
|
存储 程序员 C语言
深入理解C++:从语言特性到实践应用
深入理解C++:从语言特性到实践应用
22 3
|
12天前
|
存储 算法 安全
C++语言深度探索:从基础到实践
C++语言深度探索:从基础到实践
13 2
|
23天前
|
机器学习/深度学习 人工智能 大数据
开发语言漫谈-C++
C++最初的名字为“带类的C”
|
24天前
|
缓存 编译器 API
NumPy与其他语言(如C/C++)的接口实践
【4月更文挑战第17天】本文介绍了NumPy与C/C++的接口实践,包括Python与C/C++交互基础、NumPy的C API和Cython的使用。通过案例展示了如何将C++函数与NumPy数组结合,强调了内存管理、类型匹配、错误处理和性能优化的最佳实践。掌握这些技能对于跨语言交互和集成至关重要。
|
1月前
|
存储 C++
C++语言学习指针和引用应用案例
C++中的指针和引用用于高效操作内存。示例展示指针和引用的基本用法:指针`*p`存储变量`a`的地址,引用`&x`在函数调用中实现值交换而无需复制。此外,引用`update(&x)`可直接修改原变量,指针`p`在数组操作中用于遍历和访问不同部分。
12 2