• C++ 17 一些好用的特性


    C++11 标志着现代 C++ 的开端,C++14 在 11 的基础上查缺补漏,并未加入许多新特性,而 C++17 作为 C++11 后的第一个大版本,标志着现代 C++ 逐渐走向成熟。现在整理一下在工作中经常使用的一些特性,这些特性大体上分为三类:语法糖性能提升类型

    语法糖

    这里所说的语法糖,并不是严格意义上编程语言级别的语法糖,还包括一些能让代码更简洁更具有可读性的函数和库:

    结构化绑定

    在使用 C++17 编程的过程中发现最为便利的语法糖是结构化绑定。结构化绑定是指将 array、tuple 或 struct 的成员绑定到一组变量上的语法,常用的场景是在遍历 map/unordered_map 时不用再声明一个中间变量,然后获取相应的 key 和 value。

    1. // C++17 之前
    2. for (const auto& kv : map) {
    3. const auto& key = kv.first;
    4. const auto& value = kv.second;
    5. ......
    6. }
    7. // C++17
    8. for (const auto& [key, value] : map) {
    9. // 直接使用 key 和 value
    10. ......
    11. }

    严格来说,结构化绑定的结果并不是变量,C++ 标准称之为别名,这也导致它们不允许被 lambda 捕获,但是 gcc 并没有遵循 C++ 标准,所以以下代码在 gcc 可以编译,clang 可能编译不过:

    1. for (const auto& [key, value] : map) {
    2. [&key, &value] {
    3. std::cout << key << " : " << value << std::endl;
    4. }();
    5. }

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

    1. for(const auto& [key, value]: map){
    2. [&key = key, &value = value]{
    3. std::cout << key << ": " << value << std::endl;
    4. }();
    5. }

    另外这条限制在 C++20 中已经被删除,所以在 C++20 标准中 gcc 和 clang 都可以捕获结构化绑定的对象了。

    std::tuple 的隐式推导

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

    1. // C++17 之前
    2. std::pair<int, std::string> p1{3.14, "pi"};
    3. auto p1 = std::make_pair(3.14, "pi");
    4. // C++17
    5. std::pair p3{3.14, "pi"};

    if constexpr

    if constexpr 语句是编译期的 if 判断语句,在 C++17 以前做编译期的条件判断往往通过复杂 SFINAE机制或模版重载实现,if constexpr 大大简化了这类编程问题。比如想实现一个函数将不同类型的输入转化为字符串,在 C++17 之前可以写三个函数去实现,而在 C++17 中只需要一个函数就可以搞定。

    1. // C++17 之前
    2. template <typename T>
    3. std::string convert(T input){
    4. return std::to_string(input);
    5. }
    6. // const char* 和 string 进行特殊处理
    7. std::string convert(const char* input){
    8. return input;
    9. }
    10. std::string convert(std::string input){
    11. return input;
    12. }
    1. // C++17
    2. template <typename T>
    3. std::string convert(T input) {
    4. if constexpr (std::is_same_vconst char*> ||
    5. std::is_same_v) {
    6. return input;
    7. } else {
    8. return std::to_string(input);
    9. }
    10. }

    if 初始化语句

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

    1. // C++ 17
    2. std::map<int, std::string> m;
    3. std::mutex mx;
    4. extern bool shared_flag; // guarded by mx
    5. int demo() {
    6. // 最常用的,在 map 中查找 key,如果存在则返回想要的信息
    7. if (auto it = m.find(10); it != m.end()) {
    8. return it->second.size();
    9. }
    10. if (char buf[10]; std::fgets(buf, 10, stdin)) {
    11. m[0] += buf;
    12. }
    13. if (std::lock_guard lock(mx); shared_flag) {
    14. // 逻辑处理
    15. }
    16. if (int s; int count = Fun(&s)) {
    17. // 逻辑处理
    18. }
    19. if (const auto keywords = {"if", "for", "while"};
    20. std::ranges::any_of(keywords, [&tok](const char* kw) { return tok == kw; })) {
    21. std::cerr << "Token must not be a keyword \n";
    22. }
    23. }

    性能提升

    std::shared_mutex

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

    1. #include
    2. #include // 对于 std::unique_lock
    3. #include
    4. #include
    5. class ThreadSafeCounter {
    6. public:
    7. ThreadSafeCounter() = default;
    8. // 多个线程/读者能同时读计数器的值。
    9. unsigned int get() const {
    10. std::shared_lock lock(mutex_);
    11. return value_;
    12. }
    13. // 只有一个线程/写者能增加/写线程的值。
    14. void increment() {
    15. std::unique_lock lock(mutex_);
    16. value_++;
    17. }
    18. // 只有一个线程/写者能重置/写线程的值。
    19. void reset() {
    20. std::unique_lock lock(mutex_);
    21. value_ = 0;
    22. }
    23. private:
    24. mutable std::shared_mutex mutex_;
    25. unsigned int value_ = 0;
    26. };
    27. int main() {
    28. ThreadSafeCounter counter;
    29. auto increment_and_print = [&counter]() {
    30. for (int i = 0; i < 3; i++) {
    31. counter.increment();
    32. std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\\n';
    33. // 注意:写入 std::cout 实际上也要由另一互斥同步。省略它以保持示例简洁。
    34. }
    35. };
    36. std::thread thread1(increment_and_print);
    37. std::thread thread2(increment_and_print);
    38. thread1.join();
    39. thread2.join();
    40. }
    41. // 解释:下列输出在单核机器上生成。 thread1 开始时,它首次进入循环并调用 increment() ,
    42. // 随后调用 get() 。然而,在它能打印返回值到 std::cout 前,调度器将 thread1 置于休眠
    43. // 并唤醒 thread2 ,它显然有足够时间一次运行全部三个循环迭代。再回到 thread1 ,它仍在首个
    44. // 循环迭代中,它最终打印其局部的计数器副本的值,即 1 到 std::cout ,再运行剩下二个循环。
    45. // 多核机器上,没有线程被置于休眠,且输出更可能为递增顺序。

    std::string_view

    std::string_view 顾名思义是字符串的 "视图",类成员变量包含两个部分:字符串指针和字符串长度,std::string_view 涵盖了 std::string 的所有只读接口。std::string_view 对字符串不具有所有权,且兼容 std::string 和 const char* 两种类型。

    C++17 之前,处理只读字符串往往使用 const std::string&std::string_view 有两点性能优势:

    (1) 兼容两种字符串类型,减少类型转换和内存分配。如果传入的是字符串是 const char*,而函数的参数是 const std::string&,此时需要进行一次内存分配,将字符串拷贝到堆上,而使用 std::string_view 则可以避免这个问题。

    (2) 在处理子串时,std::string::substr 需要进行拷贝和分配内存,而std::string_view::substr 则不需要,在处理大文件解析时,性能优势非常明显。

    1. // from
    2. // author: Pavel Davydov
    3. // string_view 的 remove_prefix 比 const std::string& 的快了 15 倍
    4. string remove_prefix(const string &str) {
    5. return str.substr(3);
    6. }
    7. string_view remove_prefix(string_view str) {
    8. str.remove_prefix(3);
    9. return str;
    10. }
    11. static void BM_remove_prefix_string(benchmark::State& state) {
    12. std::string example{"asfaghdfgsghasfasg3423rfgasdg"};
    13. while (state.KeepRunning()) {
    14. auto res = remove_prefix(example);
    15. // auto res = remove_prefix(string_view(example)); for string_view
    16. if (res != "aghdfgsghasfasg3423rfgasdg") {
    17. throw std::runtime_error("bad op");
    18. }
    19. }
    20. }

    std::map/unordered_map try_emplace

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

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

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

    类型系统

    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::optionalstd::variant 和继承都是比它更高效、更合理的选择。只有当对类型完全未知的情况下,才应当使用 std::any,比如动态类型文本的解析或者业务逻辑的中间层信息传递。

    std::optional

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

    1. // C++17 之前
    2. ReturnType* func(const std::string& in) {
    3. ReturnType* ret = new ReturnType;
    4. if (in.size() == 0)
    5. return nullptr;
    6. // ...
    7. return ret;
    8. }
    9. // C++17 更安全和直观
    10. std::optional func(const string& in) {
    11. ReturnType ret;
    12. if (in.size() == 0)
    13. return nullopt;
    14. // ...
    15. return ret;
    16. }
    1. std::optional op_test(int a) {
    2. if (a < 10) return "";
    3. return std::to_string(a);
    4. }
    5. std::optional op_temp = op_test(1);
    6. if (op_temp.value().empty()) cout << "op_temp == nullptr" <
    7. op_temp = op_test(100);
    8. if (!op_temp.value().empty()) cout << "op_temp str: " << op_temp.value() << endl;

    std::variant

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

    (1) 可以存储复杂类型,而 union 只能直接存储基础的 POD 类型,对于如 std::vector 和 std::string 就等复杂类型则需要用户手动管理内存。

    (2) 类型安全,variant 存储了内部的类型信息,所以可以进行安全的类型转换,C++17 之前往往通过 union+enum 来实现相同功能。

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

    1. std::variant func(const string& in) {
    2. ReturnType ret;
    3. if (in.size() == 0)
    4. return Err{"input is empty"};
    5. // ...
    6. return {ret};
    7. }

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

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

    std::any 适用于之前使用 void* 作为通用类型的场景。

    std::optional 适用于之前使用 nullptr 代表失败状态的场景。

    std::variant 适用于之前使用 union 的场景。

  • 相关阅读:
    什么是 OpenJ9
    跳出Lambda表达式forEach()循环解决思路
    51nod 1494 选举拉票【贪心】【扫描线】【线段树】2023华为od机试真题【人气最高的店铺】
    利用opencv 做一个简单的人脸识别
    LeetCode每日一题(309. Best Time to Buy and Sell Stock with Cooldown)
    NX/UG二次开发—UF_MODL_ask_bounding_box_exact浅析
    测试工程师的简历到底怎么写?
    一场直播帮你揭开保险代打卡背后的“猫腻”
    传统语音增强——基于小波分解的语音降噪算法
    性能测试等级知识点
  • 原文地址:https://blog.csdn.net/qq_38289815/article/details/126763639