CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
  • C++17 详解 说明
  • 第一部分——语言特性
  • 1. 快速入门
  • 2. 移除或修正的语言特性
  • 3. 语言澄清(Language Clarification)
  • 4. 通用语言特性
  • 5. 模板(Templates)
  • 6. 代码标注
  • 第二部分 - 标准库的变化
  • 7. std::optional
  • 8. std::variant
  • 9. std::any
  • 10. std::string_view
  • 11. 字符串转换
  • 12. 搜索器与字符串匹配
  • 13. 文件系统
  • 14. 并行STL算法
  • 15. 标准库中的其他变化
    • 15. 标准库中的其他变化
      • std::byte
      • 额外信息
      • map和set的改进
      • 拼接
      • map和unordered_set的插入增强
      • try_emplace方法
      • insert_or_assign方法
      • 额外信息
      • 插入方法的返回类型
      • 额外信息
      • 抽样算法
      • 额外信息
      • 新的数学函数
      • 额外信息
      • 共享指针和数组
      • 额外信息
      • 非成员函数size()、data()和empty()
      • 额外信息
      • 标准库中的constexpr扩展
      • 额外信息
      • std::scoped_lock
      • 额外信息
      • 多态分配器(pmr)
      • 库中的其他变化
      • 可能的输出
      • 更多信息
      • 额外信息
      • 编译器支持
  • 16. 移除和弃用的库特性
  • 第三部分 - 更多示例和用例
  • 17. 使用std::optional和std::variant进行重构
  • 18. 使用[[nodiscard]]强制执行代码契约
  • 19. 用if constexpr替换enable_if——带可变参数的工厂函数
  • 20. 如何实现CSV读取器的并行化
目录

15. 标准库中的其他变化

# 15. 标准库中的其他变化

C++17是该语言的一次重大更新,它为标准库带来了许多特性。到目前为止,本书已经涵盖了最重要的方面,但还有很多内容有待介绍!

本书的这部分内容简要总结了标准库中的其他变化:

  • 什么是std::byte?
  • 映射(map)和集合(set)有哪些新功能?
  • 新算法:抽样(sampling)
  • 特殊数学函数
  • 共享指针(Shared Pointers)和数组
  • 非成员函数size()、data()和empty()
  • 标准库中的constexpr扩展
  • 如何使用scoped_lock锁定多个互斥锁(mutex)?
  • 什么是多态分配器(polymorphic allocator)?它如何助力内存管理?

# std::byte

std::byte是一种小型类型,它让你能够查看字节和位,而不是数字或字符值(比如unsigned char)。它被定义为枚举(enum):

enum class byte : unsigned char {} ; // 在<cstddef>中
1

你可以用unsigned char来初始化byte,这实际上是C++17的另一个便捷特性,它允许你用基础类型初始化作用域枚举。要将byte转换为数字类型,可以使用std::to_integer()。

来看一个基本示例:

// Chapter STL Other/byte.cpp
constexpr std::byte b{1};
// std::byte c{3535353}; // 错误:从int进行窄化转换
constexpr std::byte c{255};

// 移位操作:
constexpr auto b1 = b << 7;
static_assert(std::to_integer<int>(b)  == 0x01); 
static_assert(std::to_integer<int>(b1) == 0x80);

// 各种位运算符,如&、|、^等
constexpr auto c1 = b1 ^ c;
static_assert(std::to_integer<int>(c)  == 0xff); 
static_assert(std::to_integer<int>(c1) == 0x7f);

constexpr auto   c2 = ~c1;
static_assert(std::to_integer<int>(c2)  == 0x80);
static_assert(c2 == b1);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

std::byte背后的主要动机是在内存/字节访问场景下保证类型安全。

# 额外信息

查看参考论文P0298R3 (opens new window)。

# map和set的改进

在标准中,map和set有两个值得注意的特性:

  • 拼接map和set——P0083 (opens new window)
  • 新的插入方法——N4279 (opens new window)

# 拼接

现在,你可以将节点从一个基于树的容器(map/set)移动到另一个容器中,而无需额外的内存开销或分配。

例如:

// 文件名:STL Other/set_extract_insert.cpp
#include <set>
#include <string>   
#include <iostream>

struct User {
    std::string name;
    
    User(std::string s) : name(std::move(s)) {
        std::cout << "User::User(" << name << ")\n ";
    }
    ~User() {
        std::cout << "User::~User(" << name << ")\n ";
    }
    User(const User& u) : name(u.name) {
        std::cout << "User::User(copy, " << name << ")\n ";
    }
    
    friend bool operator<(const   User& u1, const   User& u2) { 
        return   u1.name < u2.name;
    }
};

int main() {
    std::set<User> setNames;
    setNames.emplace("John");   
    setNames.emplace("Alex");   
    setNames.emplace("Bartek"); 
    std::set<User> outSet;

    std::cout << "move John...\n ";
    // 将John移动到outSet
    auto handle = setNames.extract(User("John"));
    outSet.insert(std::move(handle));

    for (auto& elem : setNames)
        std::cout << elem.name << '\n ';

    std::cout << "cleanup...\n ";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

输出:

User::User(John)
User::User(Alex)
User::User(Bartek)
move John...
User::User(John)
User::~User(John)
Alex
Bartek
cleanup...
User::~User(John)
User::~User(Bartek)
User::~User(Alex)
1
2
3
4
5
6
7
8
9
10
11
12

在上述示例中,“John”这个元素从setNames中被提取并移动到outSet中。extract方法将找到的节点从集合中移出,并从物理上使其与容器分离。之后,被提取的节点可以插入到相同类型的容器中。

在C++17之前,如果你想将一个对象从一个map(或set)移动到另一个map(或set)中,你必须先从第一个容器中移除它,然后再复制或移动到另一个容器中。有了新功能,你可以操作树节点(通过使用实现定义的类型node_type),并且不会影响对象本身。这种技术可以处理不可移动的元素,当然,也更加高效。

# map和unordered_set的插入增强

在C++17中,map和unordered_set有两个新方法:

  • try_emplace()——如果对象已经存在,则不进行任何操作,否则其行为与emplace()相同。
    • 当键已经在map中时,emplace()可能会从输入参数中移动数据,所以在进行这种插入操作之前最好先使用find()查找。
  • insert_or_assign()——比operator[]提供更多信息,因为它会返回元素是新插入的还是被更新的,并且对没有默认构造函数的类型也适用 。

# try_emplace方法

下面是一个示例:

// 文件名:STL Other/try_emplace_map.cpp
#include <iostream> 
#include <string>   
#include <map>

intmain() {
    std::map<std::string, int> m;
    m["hello"] = 1;
    m["world"] = 2;

    // C++11方式:
    if(m.find("great") == std::end(m))
        m["great"] = 3;
    
    // 如果"great"不在map中,则查找操作会执行两次

    // C++17方式:
    m.try_emplace("super", 4);
    m.try_emplace("hello", 5); // 不会插入,因为它已经在map中

    for (const auto& [key, value] : m)
        std::cout << key << " -> " << value << '\n ';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

当你将元素移动到map中时,try_emplace的行为很重要:

// 文件名:STL Other/try_emplace_map_move.cpp
#include <iostream> 
#include <string>   
#include <map>

int   main() {
    std::map<std::string, std::string> m;
    m["Hello"] = "World";
    
    std::string s = "C++";
    m.emplace(std::make_pair("Hello", std::move(s)));
    
    // 字符串's'会怎样呢?
    std::cout << s << '\n ';
    std::cout << m["Hello"] << '\n ';

    s = "C++";
    m.try_emplace("Hello", std::move(s));
    std::cout << s << '\n ';
    std::cout << m["Hello"] << '\n ';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这段代码试图将["Hello", "World"]替换为["Hello", "C++"]。

如果你运行这个示例,emplace之后字符串s为空,并且“World”并没有被改为“C++”!

当键已经在容器中时,try_emplace不会执行任何操作,所以字符串s保持不变。

# insert_or_assign方法

第二个函数insert_or_assign,用于在map中插入一个新对象或分配新的值。但与operator[]不同的是,它对非默认构造类型也适用。

例如:

// 文件名:STL Other/insert_or_assign.cpp
#include <iostream> 
#include <map>
#include <string>

struct   User {
    std::string name;
    User(std::string s) : name(std::move(s)) {
        std::cout << "User::User(" << name << ")\n ";
    }
    ~User() {
        std::cout << "User::~User(" << name << ")\n ";
    }
    User(const User& u) : name(u.name) {
        std::cout << "User::User(copy, " << name << ")\n ";
    }
    
    friend bool operator<(const User& u1, const User& u2) { 
        return   u1.name < u2.name;
    }
};

int main() {
    std::map<std::string, User> mapNicks;
    //mapNicks["John"] = User("John Doe"); // 错误:User没有默认构造函数
    
    auto [iter, inserted] = mapNicks.insert_or_assign("John", User("John Doe"));
    if (inserted)
        std::cout << iter->first << " entry was inserted\n ";
    else
        std::cout << iter->first << " entry was updated\n ";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

输出:

User::User(John Doe)
User::User(copy, John Doe)
User::~User(John Doe)
John entry was inserted
User::~User(John Doe)
1
2
3
4
5

在上面的示例中,我们不能使用operator[]向容器中插入新值,因为它不支持没有默认构造函数的类型。但我们可以使用这个新函数来实现。

insert_or_assign返回一个<迭代器, bool>对。如果布尔值为true,则意味着元素被插入到容器中;否则,元素是被重新赋值。

# 额外信息

在《拼接map和set》P0083 (opens new window)和《新插入方法》N4279 (opens new window)中查看更多信息。

# 插入方法的返回类型

自C++11起,大多数标准容器都有了.emplace*方法。使用这些方法,你可以就地创建一个新对象,而无需额外的对象复制操作。

然而,在C++17之前,大多数.emplace*方法都不返回任何值,即返回类型为void。从C++17开始,这一情况发生了改变,现在它们返回插入对象的引用类型。

例如:

// C++11到C++17期间,std::vector的emplace_back方法
template < class... Args >
void emplace_back( Args&&... args );

// C++17起,std::vector的emplace_back方法
template < class... Args >
reference emplace_back( Args&&... args );
1
2
3
4
5
6
7

这一修改缩短了向容器中添加元素,然后对新添加的对象执行某些操作的代码。

例如:

// 文档中未提及emplace_return.cpp具体位置,仅按原文保留
#include <vector>
#include <string>

int main() {
    std::vector<std::string> stringVector;

    // 在C++11/14中:
    stringVector.emplace_back("Hello");
    // emplace不返回任何值,所以需要使用back()方法
    stringVector.back().append(" World");

    // 在C++17中:
    stringVector.emplace_back("Hello").append(" World");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 额外信息

更多信息请查看论文:P0084R2 (opens new window) 。

# 抽样算法

新算法std::sample,用于从序列中选择n个元素:

// 文档中未提及sample.cpp具体位置,仅按原文保留
#include <iostream>
#include <random>
#include <iterator>
#include <algorithm>

int main() {
    std::vector<int> v { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    std::vector<int> out;
    std::sample(v.begin(),                // 范围起始
                v.end(),                  // 范围结束
                std::back_inserter(out),  // 存放位置
                3,                        // 抽样元素数量
                std::mt19937{std::random_device{}()});

    std::cout << "抽样值: ";
    for (const auto &i : out)
        std::cout << i << ", ";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

可能的输出: 抽样值: 1, 4, 9,

# 额外信息

新的抽样算法源自对库基础V1技术规范(Library Fundamentals V1 TS)组件“抽样(Sampling)”(P0220R1 (opens new window) )的采用。

# 新的数学函数

C++17引入了许多新的数学函数,如std::gcd(最大公约数,Greatest Common Divisor)、std::lcm(最小公倍数,Least Common Multiple)、std::clamp以及其他特殊函数。

例如,在P0295R0 (opens new window) 中引入的std::gcm和std::lcm,在<numerics>头文件中声明:

// Chapter STL Other/sample.cpp
#include <iostream>
#include <numeric>  // 用于gcm, lcm

int main() {
    std::cout << std::gcd(24, 60) << ', ';
    std::cout << std::lcm(15, 50) << '\n ';
}
1
2
3
4
5
6
7
8

输出: 12, 150 另一个有用的函数是std::clamp(v, min, max),在<algorithm>中声明,源自P0025 (opens new window) :

// Chapter STL Other/clamp.cpp
#include <iostream>
#include <algorithm>  // clamp

int main() {
    std::cout << std::clamp(300, 0, 255) << ', ';
    std::cout << std::clamp(-10, 0, 255) << '\n ';
}
1
2
3
4
5
6
7
8

输出:255, 0 此外,在<cmath>头文件中还定义了一些新的特殊函数。

函数 描述
assoc_laguerre 计算其各自参数n、m和x的连带拉盖尔多项式
assoc_legendre 计算其各自参数l、m和x的连带勒让德函数
beta 计算其各自参数x和y的贝塔函数
comp_ellint_1 计算其各自参数k的第一类完全椭圆积分
comp_ellint_2 计算其各自参数k的第二类完全椭圆积分
comp_ellint_3 计算其各自参数k和nu的第三类完全椭圆积分
cyl_bessel_i 计算其各自参数nu和x的正则修正圆柱贝塞尔函数
cyl_bessel_j 计算其各自参数nu和x的第一类圆柱贝塞尔函数
cyl_bessel_k 计算其各自参数nu和x的非正则修正圆柱贝塞尔函数
cyl_neumann 计算其各自参数nu和x的圆柱诺伊曼函数,也称为第二类圆柱贝塞尔函数
ellint_1 计算其各自参数k和phi(phi以弧度为单位)的第一类不完全椭圆积分
ellint_2 计算其各自参数k和phi(phi以弧度为单位)的第二类不完全椭圆积分
ellint_3 计算其各自参数k、nu和phi(phi以弧度为单位)的第三类不完全椭圆积分
expint 计算其各自参数x的指数积分
hermite 计算其各自参数n和x的埃尔米特多项式
laguerre 计算其各自参数n和x的拉盖尔多项式
legendre 计算其各自参数l和x的勒让德多项式
riemann_zeta 计算其各自参数x的黎曼ζ函数
sph_bessel 计算其各自参数n和x的第一类球贝塞尔函数
sph_legendre 计算其各自参数l、m和theta(theta以弧度为单位)的球连带勒让德函数
sph_neumann 计算其各自参数n和x的球诺伊曼函数,也称为第二类球贝塞尔函数

# 额外信息

上述特殊函数在N1542 ver 3 (opens new window) 中引入。

# 共享指针和数组

在C++17之前,只有unique_ptr能够直接处理数组(无需定义自定义删除器)。现在shared_ptr也可以做到这一点。

std::shared_ptr<int []> ptr(new int [10]);
1

请注意,在C++17中std::make_shared不支持数组。但这一问题将在C++20中得到修复(参见已被合并到C++20中的P0674 (opens new window) )。

另一个重要的注意事项是应避免使用原始数组。通常使用标准容器会更好。然而,有时你可能无法使用vector或list,例如在嵌入式环境中,或者在使用第三方API时。在这种情况下,你可能会得到一个指向数组的原始指针。有了C++17,你可以将这些指针封装到智能指针(std::unique_ptr或std::shared_ptr)中,确保内存被正确释放。

# 额外信息

查看初始提案:P0414R2 (opens new window) 。

# 非成员函数size()、data()和empty()

遵循C++11中关于非成员函数std::begin()和std::end()的设计思路,C++17引入了三个新函数。

// Chapter STL Other/non_member_functions.cpp
#include <iostream>
#include <vector>

template <class Container>
void PrintBasicInfo(const Container& cont) {
    std::cout << typeid(cont).name() << '\n ';
    std::cout << std::size(cont) << '\n ';
    std::cout << std::empty(cont) << '\n ';
    if (!std::empty(cont))
        std::cout << *std::data(cont) << '\n ';
}

int main() {
    std::vector<int> iv { 1, 2, 3, 4, 5 };
    PrintBasicInfo(iv);

    float arr[4] = { 1.1f, 2.2f, 3.3f, 4.4f };
    PrintBasicInfo(arr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

输出(来自GCC 8.2):

St6vectorIiSaIiEE
5
0
1
A4_f
4
0
1.1
1
2
3
4
5
6
7
8

# 额外信息

这些新函数位于<iterator>头文件中,相关论文为N4280 (opens new window) 。

# 标准库中的constexpr扩展

通过这一增强,你可以在constexpr上下文中使用迭代器、std::array以及基于范围的for循环。

下面的示例展示了accumulate算法的constexpr基本实现(C++11/14/17版本的std::accumulate不是constexpr的):

// Chapter STL Other/constexpr_additions.cpp
#include <array>

template <typename Range, typename Func, typename T>
constexpr T SimpleAccumulate(const Range& range, Func func, T init) {
    for (auto &&obj : range) { // begin/end是constexpr
        init += func(obj);
    }
    return init;
}

constexpr int square(int i) { return i*i; }

int main() {
    constexpr std::array arr{ 1, 2, 3 }; // 类推导...

    // 使用constexpr lambda
    static_assert(SimpleAccumulate(arr, [](int i) constexpr {
        return i * i;
    }, 0) == 14);

    // 使用constexpr函数
    static_assert(SimpleAccumulate(arr, &square, 0) == 14);

    return arr[0];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

C++14编译器无法编译上述示例,但在支持C++17的情况下就可以编译。

这段代码使用了几个特性:

  • SimpleAccumulate是一个constexpr模板函数,它使用范围访问(隐藏在基于范围的for循环中)来迭代输入范围。
  • arr被推导为std::array<3, int>,这里类模板推导起作用。
  • 代码使用了constexpr lambda。
  • 使用了不带任何消息的static_assert。
  • 自C++17起,std::begin()和std::end()也是constexpr的。

你还可以在关于通用语言特性的“constexpr lambda”章节中看到constexpr扩展的另一个示例。

每个C++标准都允许越来越多的代码成为constexpr。在C++17中,我们可以开始在常量表达式中使用基本容器。在C++20中,我们将获得更多声明为constexpr的标准算法。

# 额外信息

主要参考论文是P0031 - 《向反向迭代器、移动迭代器、数组和范围访问添加constexpr修饰符的提案 (opens new window)》(Proposal to Add Constexpr Modifiers to reverse_iterator, move_iterator, array and Range Access) 。

# std::scoped_lock

在C++11和C++14中,我们有了线程库以及许多支持功能。

例如,使用std::lock_guard,你可以以RAII(Resource Acquisition Is Initialization,资源获取即初始化)风格获取互斥锁(mutex)的所有权并锁定它:

std::mutex m;
std::lock_guard<std::mutex> lock_one(m);
// lock_one离开作用域时解锁...
1
2
3

上述代码仅适用于单个互斥锁。如果你想要锁定多个互斥锁,就必须使用不同的模式,例如:

std::mutex first_mutex;
std::mutex second_mutex;
// ...
std::lock(fist_mutex, second_mutex);
std::lock_guard<std::mutex> lock_one(fist_mutex, std::adopt_lock);
std::lock_guard<std::mutex> lock_two(second_mutex, std::adopt_lock);
// ..
1
2
3
4
5
6
7

在C++17中,事情变得更简单了,使用std::scoped_lock你可以同时锁定多个互斥锁。

std::scoped_lock lck(first_mutex, second_mutex);
1

由于兼容性问题,std::lock_guard无法扩展为接受多个输入互斥锁,这就是需要新类型scoped_lock的原因。

# 额外信息

你可以在P0156 (opens new window)中了解更多信息。

# 多态分配器(pmr)

多态分配器是对标准库中标准分配器的增强。

简而言之,多态分配器遵循标准库中分配器的规则,但核心是它使用内存资源对象来执行内存管理。多态分配器包含一个指向内存资源类的指针,这就是它可以使用虚方法调度的原因。你可以在运行时更改内存资源,同时保持分配器的类型不变。

多态分配器的所有类型都位于单独的命名空间std::pmr(PMR代表多态内存资源,Polymorphic Memory Resource)中,在<memory_resource>头文件中定义。

pmr的核心元素:

  • std::pmr::memory_resource:所有其他实现的抽象基类。它定义了以下纯虚方法:do_allocate、do_deallocate和do_is_equal。
  • std::pmr::polymorphic_allocator:标准分配器的一种实现,它使用memory_resource对象来执行内存分配和释放操作。
  • 通过new_delete_resource()和null_memory_resource()访问的全局内存资源。
  • 一组预定义的内存池资源类:
    • synchronized_pool_resource
    • unsynchronized_pool_resource
    • monotonic_buffer_resource
  • 具有多态分配器的标准容器的模板特化,例如std::pmr::vector、std::pmr::string、std::pmr::map等。每个特化都在与相应容器相同的头文件中定义。

# 库中的其他变化

以下是对预定义内存资源的简要概述:

资源 描述
new_delete_resource() 一个自由函数,返回指向全局 “默认” 内存资源的指针。它使用全局的new和delete来管理内存
null_memory_resource() 一个自由函数,返回指向全局 “空” 内存资源的指针,每次分配内存时都会抛出std::bad_alloc异常
synchronized_pool_resource 线程安全的分配器,管理不同大小的内存池。每个内存池由一组块组成,这些块被划分为大小统一的内存块
unsynchronized_pool_resource 非线程安全的内存池资源
monotonic_buffer_resource 非线程安全、快速的专用资源,从预分配的缓冲区获取内存,但释放时不会归还内存。值得一提的是,池资源(包括monotonic_buffer_resource)可以链式连接。因此,如果某个内存池中没有可用内存,分配器将从 “上游” 资源分配内存

下面是monotonic_buffer_resource和pmr::vector的一个简单示例:

// Chapter STL Other/pmr_monotonic_resource.cpp
#include <iostream>
#include <memory_resource>
#include <vector>

int main() {
    char buffer[64] = {}; 
    std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_ ');
    std::cout << buffer << '\n ';
    std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
    std::pmr::vector<char> vec{ &pool };
    for (char ch = 'a '; ch <= 'z '; ++ch)
        vec.push_back(ch);
    std::cout << buffer << '\n ';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 可能的输出

aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz
1

在上述示例中,我们使用了一个由栈上内存块初始化的单调缓冲区资源。通过使用一个简单的char buffer[]数组,我们可以轻松打印 “内存” 的内容。向量从内存池中获取内存,如果没有更多可用空间,它将向 “上游” 资源(默认是new_delete_resource)请求内存。该示例展示了向量在需要插入更多元素时的重新分配情况。每次向量获取更多空间,最终可以容纳所有字母。

# 更多信息

本节只是触及了多态分配器和内存资源的概念。如果你想深入了解这个主题,尼古拉·约苏蒂斯(Nicolai Josuttis)所著的《C++17完全指南》中有一整章的内容。也有一些会议演讲,例如2017年C++大会(CppCon 2017)上巴勃罗·哈尔彭(Pablo Halpern)的《分配器:精华部分 (opens new window)》。

# 额外信息

更多信息请参见P0220R1 (opens new window)和P0337R0 (opens new window)。

# 编译器支持

特性 GCC Clang MSVC
std::byte 7.1 5.0 VS 2017 15.3
映射(Maps)和集合(Sets)的改进 7.0 3.9 VS 2017 15.5
映射的insert_or_assign() / try_emplace() 6.1 3.7 VS 2017 15
emplace返回类型 7.1 4.0 VS 2017 15.3
抽样算法 7.1 开发中 VS 2017 15
最大公约数(gcd)和最小公倍数(lcm) 7.1 4.0 VS 2017 15.3
clamp 7.1 3.9 VS 2015.3
特殊数学函数 7.1 未支持 VS 2017 15.7
共享指针和数组 7.1 开发中 VS 2017 15.5
非成员函数size()、data()和empty() 6.1 3.6 VS 2015
标准库的constexpr扩展 7.1 4.0 VS 2017 15.3
作用域锁(scoped_lock) 7.1 5.0 VS 2017 15.3
多态分配器和内存资源 9.1 开发中 VS 2017 15.6
上次更新: 2025/05/12, 17:14:47
14. 并行STL算法
16. 移除和弃用的库特性

← 14. 并行STL算法 16. 移除和弃用的库特性→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式