第 34 章 内存和资源管理(Memory and Resources)
目录
34.3 资源管理指针(Resource Management Pointers)
34.4.2 分配器特征(Allocator Traits)
34.4.4 作用域分配器(Scoped Allocators)
34.5 垃圾收集接口(The Garbage Collection Interface)
34.6 未初始化内存(Uninitialized Memory)
34.6.1 临时缓存(Temporary Buffers)
34.1 引言
STL(第31章,第32章,第33章)是用于管理和操作数据的标准库工具中结构化程度最高、通用性最强的部分。本章介绍一些更专业或处理原始内存(而非类型化对象)的工具。
34.2 准容器(“Almost Containers”)
标准库提供了一些与 STL 框架并不完美契合的容器(§31.4,§32.2,§33.1)。例如内置数组,array和string。我有时会将它们称为“准容器”(§31.4),但这不太公平:它们可以保存元素,因此它们是容器,但每一个容器都存在一些限制或附加功能,这使得它们在 STL 的上下文中显得有些不合适。单独描述它们也简化了 STL 的描述。
“准容器” | |
T[N] | 内置数组:一个固定大小、连续分配的序列,包含 N 个元素,类型为 T;隐式转换为 T∗ |
array<T,N> | 一个固定大小、连续分配的序列,包含 N 个元素,类型为 T;类似内置数组,但解决了大多数内置数组存在的问题 |
bitset<N> | N 位固定大小序列 |
vector<bool> | 以vector特化形式紧凑存储的位序列 |
pair<T,U> | 两个类型为 T 和 U 的元素 |
tuple<T...> | 任意数量、任意类型的元素的序列 |
basic_string<C> | C 类型的字符序列;提供字符串操作 |
valarray<T> | T 类型的数值数组;提供数字运算 |
为什么标准库提供这么多容器?它们满足一些常见但又不同(通常重叠)的需求。如果标准库不提供这些容器,许多人就不得不自己设计和实现。例如:
• pair 和 tuple 是异构的;所有其他容器都是同构的(所有元素均为同一类型)。
• array, vector 和 tuple 的元素是连续分配的;forward_list 和 map 是链式结构。
• bitset 和vector<bool> 保存位并通过代理对象访问它们;所有其他标准库容器都可以保存各种类型并直接访问元素。
• basic_string 要求其元素为某种形式的字符,并提供字符串操作,例如连接(concatenation)和本地敏感操作(locale-sensitive)(第 39 章);而 valarray 要求其元素为数值,并提供数值操作。
所有这些容器都可以视为提供大量程序员所需的特化服务。没有哪个容器能够满足所有这些需求,因为有些需求是相互矛盾的,例如,“可增长”与“保证分配在固定位置”,以及“添加元素时元素不移动”与“连续分配”。此外,一个非常通用的容器可能意味着单个容器无法接受的开销。
34.2.1 array
<array> 中定义的array是给定类型的元素的固定大小序列,其中元素的数量在编译时指定。因此,array可以分配到栈、对象或静态存储中。元素分配在定义array的作用域内。最好将array理解为内置数组,其大小固定不变,无需进行隐式的、可能令人意外的指针类型转换,并提供一些便捷函数。与使用内置数组相比,使用array不会产生任何额外开销(时间或空间)。array不遵循 STL 容器的“元素句柄”模型。相反,array直接包含其元素:
template<typename T, size_t N> // N个T的数组 (§iso.23.3.2)
struct array {
/*
类似于vector的类型和操作 (§31.4),
只是那些改变容器大小,构造函数和赋值函数的操作除外
*/
void fill(const T& v);// 赋值v的N个副本
void swap(array&) noexcept(noexcept(swap(declval<T&>(), declval<T&>())));
T __elem[N]; // 实现细节
};
array中不存储任何“管理信息”(例如大小)。这意味着移动(§17.5) array并不比复制数组更高效(除非array元素是可高效移动的资源句柄)。array没有构造函数或分配器(因为它不直接分配任何内容)。
array的元素个数和下标值都是无符号类型(size_t),类似于vector,但与内置数组不同。因此,array<int,−1> 可能会被粗心的编译器接受。希望给出警告。
array可以通过初始化列表进行初始化:
array<int,3> a1 = { 1, 2, 3 };
初始化器中的元素数量必须等于或小于array指定的元素数量。通常,如果初始化器列表为部分元素(而非全部元素)提供了值,则其余元素将使用适当的默认值进行初始化。例如:
void f()
{
array<string, 4> aa = {"Churchill", "Clare"};
//
}
最后两个元素将是空字符串。
元素数量不可省略,必需显式指定:
array<int> ax = { 1, 2, 3 }; // 错误:大小未指定
为了避免特殊情况,元素的数量可以为零:
int<int,0> a0;
元素数量必须是一个常量表达式:
void f(int n)
{
array<string,n> aa = {"John's", "Queens' "}; // 错: 大小不是常量表达式
//
}
如果需要元素数量可变,请使用 vector 。在另一方面,由于array的元素数量在编译时已知,因此array的 size() 是一个 constexpr 函数。
array没有复制参数值的构造函数(vector有;§31.3.2)。相反,它提供了一个 fill() 操作:
void f()
{
array<int,8> aa; // 未初始化
aa.fill(99); // 赋值8个 99 的副本
// ...
}
由于array不遵循“元素句柄”模型,因此 swap() 必须实际交换元素,因此交换两个 array<T,N> 会将 swap() 应用于 N 对 T 。array<T,N>::swap() 的声明基本上表明,如果 T 的 swap() 可以抛出异常,那么 array<T,N> 的 swap() 也可以抛出异常。显然,应该像躲避瘟疫一样避免抛出 swap() 异常。
必要时,可以将array显式传递给需要指针的 C 风格函数。例如:
void f(int∗ p, int sz); // C风格接口
void g()
{
array<int,10> a;
f(a,a.size()); // 错: 没有这样的转换
f(&a[0],a.size()); // C风格用法
f(a.data(),a.size()); // C风格用法
auto p = find(a.begin(),a.end(),777); // C++/STL风格用法
// ...
}
既然vector灵活得多,我们为什么要使用array呢?因为array灵活性较差,所以更简单。有时,直接访问在堆上分配的元素,而不是在自由存储空间上分配元素,再通过vector(句柄)间接访问,然后再释放它们,会带来显著的性能优势。在另一方面,栈是一种有限的资源(尤其是在某些嵌入式系统上),栈溢出非常严重。
既然可以使用内置数组,为什么还要使用array呢?array知道自己的大小,所以很容易使用标准库算法,而且可以复制(使用 = 或初始化)。然而,我更喜欢使用array的主要原因是它能让我避免那些令人讨厌的指针转换。考虑一下:
void h()
{
Circle a1[10];
array<Circle,10> a2;
// ...
Shape∗ p1 = a1; // OK: 即将发生灾难行为
Shape∗ p2 = a2; // 错: 无array<Circle,10> 到 Shape* 的转换
p1[3].draw(); //灾难发生
}
“灾难”注释假设 sizeof(Shape) < sizeof(Circle),因此通过 Shape∗ 下标 Circle[] 会得出错误的偏移量 (§27.2.1,§17.5.1.4)。所有标准容器都比内置数组具有这一优势。
array可以看作一个tuple(§34.2.4),其中所有元素都属于同一类型。标准库支持这种观点。Tuple 辅助类型函数 tuple_size 和 tuple_element 可以应用于array:
tuple_size<array<T,N>>::value //N
tuple_element<S,array<T,N>>::type // T
我们还可以使用 get<i> 函数来访问第 i 个元素:
template<size_t index, typename T, siz e_t N>
T& get(array<T,N>& a) noexcept;
template<size_t index, typename T, siz e_t N>
T&& get(array<T,N>&& a) noexcept;
template<size_t index, typename T, siz e_t N>
const T& get(const array<T,N>& a) noexcept;
例如:
array<int,7> a = {1,2,3,5,8,13,25};
auto x1 = get<5>(a); // 13
auto x2 = a[5]; // 13
auto sz = tuple_size<decltype(a)>::value; // 7
typename tuple_element<5,decltype(a)>::type x3 = 13; // x3 is an int
这些类型函数适用于编写需要 tuple 的代码的人。
使用 constexpr 函数(§28.2.2)和类型别名(§28.2.1)来提高可读性:
auto sz = Tuple_siz e<decltype(a)>(); // 7
Tuple_element<5,decltype(a)> x3 = 13; // x3 is an int
tuple 语法旨在用于泛型代码。
34.2.2 bitset
系统的各个方面,例如输入流的状态(§38.4.5.1),通常表示为一组标志,指示二进制状态,例如好/坏、真/假和开/关。C++ 通过对整数进行按位运算 (§11.1.1),有效地支持小型标志集的概念。bitset<N> 类推广了这一概念,并通过提供对 N 位序列 [0:N) 的操作(其中 N 在编译时已知)提供了更大的便利。对于无法装入 long long int 的位集,使用 bitset 比直接使用整数更方便。对于较小的集合,bitset 通常经过优化。如果你想命名位而不是编号,可以选择使用 set (§31.4.3),枚举 (§8.4) 或位域 (§8.2.7)。
bitset<N> 是一个包含 N 位的数组。它由 <bitset> 表示。bitset 与 vector<bool> (§34.2.3) 的不同之处在于其大小固定;与 set (§31.4.3) 的不同之处在于其位是通过整数而不是值进行关联索引的;与 vector<bool> 和 set 的不同之处在于,bitset 提供了操作位的操作符(译注:其实还有占用空间大小的区别)。
使用内置指针(§7.2)无法直接访问单个位。因此,bitset 提供了一种引用位(代理)类型。这实际上是一种非常实用的技术,用于访问那些由于某些原因内置指针无法访问的对象:
template<size_t N>
class bitset {
public:
class reference { // 引用单个位:
friend class bitset;
reference() noexcept;
public: //suppor t zero-based subscripting in [0:b.size())
˜reference() noexcept;
reference& operator=(bool x) noexcept; // for b[i] = x;
reference& operator=(const reference&) noexcept; // for b[i] = b[j];
bool operator˜() const noexcept; // return ˜b[i]
operator bool() const noexcept; // for x = b[i];
reference& flip() noexcept; // b[i].flip();
};
// ...
};
由于历史原因,bitset 的样式与其他标准库类不同。例如,如果索引(也称为位位置)超出范围,则会抛出 out_of_range 异常。bitset 不提供迭代器。位位置的编号方式通常与位在字中的编号方式相同,都是从右向左编号,因此 b[i] 的值为 pow(2,i)。因此,bitset 可以视为一个 N 位二进制数:
34.2.2.1 构造函数
可以使用指定数量的零,来自 unsigned long long int 中的位或来自 string 来构造位集:
bitset<N>构造函数(§iso.20.5.1) | |
bitset bs {}; | N个0位 |
bitset bs {n}; | 来自 n 个位;n 是无符号 long long |
bitset bs {s,i,n,z,o}; | s 的 n 位 [i:i+n);s 为 basic_string<C,Tr,A>; z(ero) 为用于表示0的 C 类型字符(位置0); o(ne) 为用于表示1的 C 类型字符(位置1);显式 |
bitset bs {s,i,n,z}; | bitset bs {s,i,n,z,C{’1’}}; |
bitset bs {s,i,n,z}; | bitset bs {s,i,n,C{’0’},C{’1’}};} |
bitset bs {s,i}; | bitset bs {s,i,npos,C{’0’},C{’1’}}; |
bitset bs {s}; | bitset bs {s,0,npos,C{’0’},C{’1’}}; |
bitset bs {p,n,z,o}; | n 位 [p:p+n];p 为 C 风格字符串,类型为 C∗; z 为 C 类型字符,用于表示位置0; o 为 C 类型字符,用于表示一位置1;显式 |
bitset bs {p,n,z}; | bitset bs {p,n,z,C{’0’}}; |
bitset bs {p,n}; | bitset bs {p,n,C{’1’},C{’0’}}; |
bitset bs {p}; | bitset bs {p,npos,C{’1’},C{’0’}}; |
位置 npos 是 string<C> 的“超出末尾”位置,即“直到末尾的所有字符”(§36.3)。
当传入一个 unsigned long long int 参数时,该整数中的每个位都会用于初始化位组中的相应位(如果有)。basic_string(§36.3)参数的作用与此相同,但字符“0”表示位值为 0,字符“1”表示位值为 1,其他字符则会引发 invalid_argument 异常。例如:
void f()
{
bitset<10> b1; // 全0
bitset<16> b2 = 0xaaaa; // 1010101010101010
bitset<32> b3 = 0xaaaa; // 00000000000000001010101010101010
bitset<10> b4 {"1010101010"}; // 1010101010
bitset<10> b5 {"10110111011110",4}; // 0111011110
bitset<10> b6 {string{"1010101010"}}; // 1010101010
bitset<10> b7 {string{"10110111011110"},4}; // 0111011110(注意从右向左)
bitset<10> b8 {string{"10110111011110"},2,8}; // 0011011101(注意从右向左)
bitset<10> b9 {string{"n0g00d"}}; // 抛出invalid_argument 异常
bitset<10> b10 = string{"101001"}; // 错误: 不存在string 到 bitset 的隐式转换
}
译注:注意位的排序是从右向左,左边最高位,右边最低位。
bitset 设计的一个关键理念是,能够为适合单个字的位集提供优化的实现。接口体现了这一假设。
34.2.2.2 bitset 操作
bitset 提供了用于访问单个位以及操作集合中的所有位的运算符:
bitset<N> 操作(§iso.20.5) | |
bs[i] | bs的第i位 |
bs.test(i) | bs的第i位;若i不在 [0:bs.size()) 中,则抛出 out_of_range 异常 |
bs&=bs2 | 接位与 |
bs|=bs2 | 按位或 |
bsˆ=bs2 | 按位异或(按位排他或) |
bs<<=n | 逻辑左移(用0填充空位) |
bs>>=n | 逻辑右移(用0填充空位) |
bs.set() | 置bs的每一位为1 |
bs.set(i,v) | 置bs[i]=v |
bs.reset() | 置bs的每一位为0 |
bs.reset(i) | bs[i]=0 |
bs.flip() | 对于bs的每一位,置 bs[i]=˜bs[i] |
bs.flip(i) | bs[i]=˜bs[i] |
bs2=˜bs | 求补集:bs2=bs, bs2.flip() |
s2=bs<<n | 生成左移集:bs2=bs, bs2<<=n |
bs2=bs>>n | 生成右移集:bs2=bs, bs2>>=n |
bs3=bs&bs2 | 按位与:对于 bs 中的每一个位,执行操作bs3[i]=bs[i]&bs2[i] |
bs3=bs|bs2 | 按位或:对于 bs 中的每一个位,执行操作bs3[i]=bs[i]|bs2[i] |
bs3=bsˆbs2 | 按位异或:对于 bs 中的每一个位,执行操作bs3[i]=bs[i]ˆbs2[i] |
is>>bs | 从 is 读入 bs ,is 是一个istream |
os<<bs | 将bs写入os,os 是一个ostream |
当 >> 和 << 的第一个操作数是 iostream 时,它们是 I/O 运算符;否则,它们是移位运算符,并且它们的第二个操作数必须是整数。例如:
bitset<9> bs ("110001111"};
cout << bs << '\n'; // write "110001111" to cout
auto bs2 = bs<<3; // bs2 == "001111000";
cout << bs2 << '\n'; // write "001111000" to cout
cin >> bs; // read from cin
bs2 = bs>>3; //若输入是 "110001111",则 bs2 == "000110001"
cout << bs2 << '\n'; // write "000110001" to cout
位移位时,采用逻辑移位(而非循环移位)。这意味着某些位会“偏离末尾”,而某些位置会获得默认值 0。需要注意的是,由于 size_t 是无符号类型,因此无法进行负数移位。然而,当 b<<-1 时,这意味着会进行一个非常大的正值移位,从而使 bitset b 中的每一个位都保留 0 值。编译器应该会对此发出警告。
bitset 还支持常见操作,例如 size(),==,I/O 等:
更多关于bitset<N> 的操作(§iso.20.5) C、Tr 和 A 对 basic_string<C,Tr,A> 有默认值 | |
n=bs.to_ulong() | n 是与 bs 相对应的 unsigned long |
n=bs.to_ullong() | n 是与 bs 相对应的 unsigned long long |
s=bs.to_string<C,Tr,A>(c0,c1) | s[i]=(b[i])?c1:c0; s 是一个basic_string<C,Tr,A> |
s=bs.to_string<C,Tr,A>(c0) | s=bs.template to_string<C,Tr,A>(c0,C{’1’}) |
s=bs.to_string<C,Tr,A>() | s=bs.template to_string<C,Tr,A>(C{’0’},C{’1’}) |
n=bs.count() | n 是 bs 中值为 1 的位数 |
n=bs.size() | n 是 bs 中的位数 |
bs==bs2 | bs 和bs2是否具有相同值 |
bs!=bs2 | !(bs==bs2) |
bs.all() | bs 中的所有位值都为1吗? |
bs.any() | bs 中的任意位值都为1吗? |
bs.none() | bs 中的没有位值为1吗? |
hash<bitset<N>> | Hash 对bitset<N>的特化 |
to_ullong() 和 to_string() 操作提供与构造函数相反的操作。为了避免不明显的转换,优先使用命名操作而不是转换运算符。如果 bitset 的值的有效位过多,以至于无法表示为无符号长整型,则 to_ulong() 会抛出 overflow_error;如果位集参数无法容纳,则to_ullong() 也会抛出 overflow_error。
幸运的是,to_string 返回的 basic_string 的模板参数是默认的。例如,我们可以写出 int 的二进制表示:
void binary(int i)
{
bitset<8∗sizeof(int)> b = i; // assume 8-bit byte (see also §40.2)
cout << b.to_string<char,char_traits<char>,allocator<char>>() << '\n'; // general and verbose
cout << b.to_string<char>() << '\n'; // use default traits and allocator
cout << b.to_string<>() << '\n'; // use all defaults
cout << b.to_string() << '\n'; // use all defaults
}
这将从左到右打印表示为 1 和 0 的位,最高有效位放在最左边,因此参数 123 将给出输出
00000000000000000000000001111011
00000000000000000000000001111011
00000000000000000000000001111011
00000000000000000000000001111011
对于这个例子来说,直接使用 bitset 输出运算符更简单:
void binary2(int i)
{
bitset<8∗sizeof(int)> b = i; // 假设为 8 位一个字节 (另见 §40.2)
cout << b << '\n';
}
34.2.3 vector<bool>
<vector> 中的 vector<bool> 是vector(§31.4)的特化,提供位(bool)的紧凑存储:
template<typename A>
class vector<bool,A> { // specialization of vector<T,A> (§31.4)
public:
using const_reference = bool;
using value_type = bool;
// like vector<T,A>
class reference { // suppor t zero-based subscripting in [0:v.size())
friend class vector;
reference() noexcept;
public:
˜reference();
operator bool() const noexcept;
reference& operator=(const bool x) noexcept; // v[i] = x
reference& operator=(const reference& x) noexcept; // v[i] = v[j]
void flip() noexcept; // flip the bit: v[i]=˜v[i]
};
void flip() noexcept; // 反转 v 的所有位,true变更为false,或反之
// ...
};
与 bitset 的相似性是显而易见的,但是,与 bitset 不同但与 vector<T> 类似,vector<bool> 具有分配器并且可以改变其大小(译注:因此占用更多内存)。
与 vector<T> 一样,vector<bool> 中索引较高的元素具有较高的地址:
这与 bitset 中的布局完全相反。此外,它也不直接支持整数和字符串与 vector<bool> 之间的相互转换。
使用 vector<bool> 就像使用其他 vector<T> 一样,但单个位操作的效率会低于vector<char> 的等效操作。此外,在 C++ 中,不可能完全忠实地模拟(内置)引用与代理的行为,因此在使用 vector<bool> 时,不要试图区分右值/左值。
34.2.4 元组(Tuples)
标准库提供了两种将任意类型的值分组到单个对象的方法:
• 一个 pair(§34.2.4.1)包含两个值。
• 一个 tuple(§34.2.4)包含零个或多个值。
当我们需要(静态地)知道恰好有两个值时,我们会使用 pair。而使用 tuple 时,我们总是需要处理所有可能的值数量。
34.2.4.1 pair
在 <utility> 中,标准库提供了用于操作数值对的类 pair:
template<typename T, typename U>
struct pair {
using first_type = T; // the type of the first element
using second_type = U; // the type of the second element
T first; //first element
U second; // second element
// ...
};
pair<T,U>(§iso.20.3.2) | |
pair p {} | 默认构造函数:pair p {T{},U{}}; constexpr |
pair p {x,y} | p.first 初始化为 x,p.second 初始化为 y |
pair p {p2} | 从对 p2 构造:对 p {p2.first,p2.second}; |
pair p {piecewise_construct,t,t2} | p.first 由元组 t 的元素构成,p.second 由元组 t2 的元素构成 |
p.˜pair() | 析构函数:销毁 t.first 和 t.second |
p2=p | 复制分配:p2.first=p.first 和 p2.second=p.second |
p2=move{p} | 移动赋值:p2.first=move(p.first) 且 p2.second=move(p.second) |
p.swap(p2) | 交换 p 和 p2 的值 |
对 pair 的操作不成立,前提是其元素上的相应操作成立。同样,如果对 pair 的元素上的相应操作成立,则复制或移动操作成立。
元素 first 和 second 是可以直接读写的成员。例如:
void f()
{
pair<string,int> p {"Cambridge",1209};
cout << p.first; // print "Cambr idge"
p.second += 800; // update year
// ...
}
piecewise_construct 是 piecewise_construct_t 类型的对象的名称,用于区分使用tuple 类型成员构造一个 pair ,以及使用 tuple 作为其 first 和 second 参数列表构造一个 pair 。例如:
struct Univ {
Univ(const string& n, int r) : name{n}, rank{r} { }
string name;
int rank;
string city = "unknown";
};
using Tup = tuple<string,int>;
Tup t1 {"Columbia",11}; // U.S. News 2012
Tup t2 {"Cambridg e",2};
pair<Tub,Tub> p1 {t1,t2}; // pair of tuples
pair<Univ,Univ> p2 {piecewise_construct,t1,t2}; // pair of Univs
即,p1.second 是 t2,即 {"Cambridge",2}。相比之下,p2.second 是 Univ{t2},即 {"Cambridge", 2,"unknown"}。
pair<T,U> 辅助功能(§iso.20.3.3,(§iso.20.3.4) | |
p==p2 | p.first==p2.first && p.second==p2.second |
p<p2 | p.first<p2.first || (!(p2.first<p.first) && p.second<p2.second) |
p!=p2 | !(p==p2) |
p>p2 | p2<p |
p<=p2 | !(p2<p) |
p>=p2 | !(p<p2) |
swap(p,p2) | p.swap(p2) |
swap(p,p2) | p 是一对 <decltype(x),decltype(y)>,分别保存 x 和 y 的值;如果可能,移动 x 和 y,而不是复制 |
tuple_size<T>::value | 一个类型 T 的 pair |
tuple_element<N,T>::type | first (若 N == 0 )或second (若 N == 1 )的类型 |
get<N>(p) | p pair 中第 N 个元素的引用;N 必须为 0 或 1 |
make_pair 函数避免明确提及 pair 的元素类型。例如:
auto p = make_pair("Harvard",1736);
34.2.4.2 tuple
在 <tuple> 中,标准库提供了 tuple 类及其各种支持功能。一个 tuple 是由 N 个任意类型的元素组成的序列:
template<typename... Types>
class tuple {
public:
// ...
};
元素的数量为零或正数。
有关tuple设计、实现和使用的详细信息,请参阅§28.5 和§28.6.4。
tuple<Types...> 成员(§iso.20.4.2) | |
tuple t {}; | 默认构造函数:空元组;constexpr |
tuple t {args}; | t 对于 args 的每个元素都有一个元素;explicit |
tuple t {t2}; | 从 tuple t2 构造 |
tuple t {p}; | 从 pair p 构造 |
tuple t {allocator_arg_t,a,args}; | 使用分配器 a 从 args 构造 |
tuple t {allocator_arg_t,a,t2}; | 使用分配器 a 从tuple t2 构造 |
tuple t {allocator_arg_t,a,p}; | 使用分配器 a 从 pair p 构造 |
t.˜tuple() | 析构函数,销毁每一个元素 |
t=t2 | tuple 复制赋值 |
t=move(t2) | tuple 移动赋值 |
t=p | pair p 复制赋值 |
t=move(p) | pair p 移动赋值 |
t.swap(t2) | 交换 t 和 t2 的值;noexcept |
tuple 的类型、= 的操作数以及 swap() 的参数等的类型不必相同。当(且仅当)元素上的隐含操作有效时,操作才有效。例如,如果被赋值的 tuple 中的每一个元素都可以赋值给目标元素,那么我们就可以将一个 tuple 赋值给另一个元组。例如:
tuple<string,vector<double>,int> t2 = make_tuple("Hello, tuples!",vector<int>{1,2,3},'x');
如果所有元素操作都为 noexcept,则操作为 noexcept;只有当成员操作抛出异常时,操作才会抛出异常。同样,如果 tuple 操作为 constexpr,则元组操作为 constexpr 。
一对操作数(或参数)的每一个 tuple 中元素的数量必须相同。
tuple<int,int,int> rotate(tuple<int,int,int> t)
{
return {t.get<2>(),t.g et<0>(),t.get<1>()}; // 错: 显式 tuple 构造函数
}
auto t2 = rotate({3,7,9}); // 错: 显式 tuple 构造函数
如果您只需要两个元素,则可以使用 pair:
pair<int,int> rotate(pair<int,int> p)
{
return {p.second,p.first};
}
auto p2 = rotate({3,7});
更多例子,见 §28.6.4 。
tuple<Types...> 辅助功能(§iso.20.4.2.4,§iso.20.4.2.9) | |
t=make_tuple(args) | 从 args 创建 tuple |
t=forward_as_tuple(args) | t 是指向 args 中元素的右值引用tuple,因此你可以通过 t 转发 arg 的元素 |
t=tie(args) | 是对 args 元素的左值引用的tuple,因此你可以通过 t 分配给 args 元素 |
t=tuple_cat(args) | 连接元组:args 是一个或多个tuple;t 按顺序包含 args 中的tuple成员 |
tuple_size<T>::value | tuple T 的元素数量 |
tuple_elements<N,T>::type | tuple T 的第N个元素类型 |
get<N>(t) | tuple t 的第N个元素的引用 |
t==t2 | t和t2的所有元素都相等吗?如果相等,则t和t2一定有相同的元素数量 |
t!=t2 | !(t==t2) |
t<t2 | 按字典顺序排列,t 是否小于 t2? |
t>t2 | t2<t |
t<=t2 | !(t2>t) |
t>=t2 | !(t2<t) |
uses_allocator<T,A>::value | tuple<T> 可以由 A 类型的分配器分配吗? |
swap(t,t2) | t.swap(t2) |
例如,tie() 可用于从元组中提取元素:
auto t = make_tuple(2.71828,299792458,"Hannibal");
double c;
string name;
tie(c,ignore,name) = t; // c=299792458; name="Hannibal"
名称“ignore”指的是忽略赋值操作的对象类型。因此,tie() 中的“ignore”意味着尝试赋值到其 tuple 位置的操作会被忽略。另一种方法是:
double c = get<0>(t); // c=299792458
string name = get<2>(t); // name="Hannibal"
显然,如果 tuple 来自“其他地方”,这样我们就无法轻易知道元素的值,那么情况会更有趣。例如:
tuple<int,double ,string> compute();
// ...
double c;
string name;
tie(c,ignore,name) = t; // 取出的结果放入 c 和 name 中
34.3 资源管理指针(Resource Management Pointers)
指针指向一个对象(或不指向)。然而,指针并不表明谁(如果有的话)拥有这些对象。也就是说,仅仅看指针,我们不知道谁应该删除它指向的对象,也不知道如何删除,甚至根本不知道是否删除。在 <memory> 中,我们寻找“智能指针”来表达所有权:
• unique_ptr (§34.3.1) 表示独占所有权
• shared_ptr (§34.3.2) 表示共享所有权
• weak_ptr (§34.3.3) 表示循环共享数据结构中的循环
这些资源句柄在§5.2.1 中介绍。
34.3.1 unique_ptr
unique_ptr(在 <memory> 中定义)提供了严格所有权的语义:
• unique_ptr 拥有其指针所指向的对象。也就是说,unique_ptr 有义务销毁其指针所指向的对象(如果有)。
• unique_ptr 无法复制(没有复制构造函数或复制赋值)。但是,它可以移动。
• unique_ptr 存储一个指针,并在其自身销毁时(例如,当控制线程离开 unique_ptr 的作用域时;§17.2.2),使用关联的删除器(如果有)删除指向的对象(如果有)。
unique_ptr 的用途包括:
• 为动态分配的内存提供异常安全(§5.2.1,§13.3)
• 将动态分配内存的所有权传递给函数
• 从函数返回动态分配的内存
• 将指针存储在容器中
将 unique_ptr 视为由简单指针(“包含的指针”)或(如果它具有删除器)一对指针表示:
当 unique_ptr 被销毁时,会调用其删除器来销毁其所属的对象。删除器代表了销毁对象的含义。例如:
• 局部变量的删除器不应执行任何操作。
• 内存池的删除器应将对象返回到内存池并销毁(或不销毁),具体取决于内存池的定义方式。
• unique_ptr 的默认版本(“无删除器”)使用 delete 函数。它甚至不存储默认删除器。它可以是特化版本,也可以依赖于空基优化(§28.5)。
这样,unique_ptr 支持通用资源管理(§5.2)。
template<typename T, typename D = default_delete<T>>
class unique_ptr {
public:
using pointer = ptr; // 所饮食的指针类型
// ptr is D::pointer if that is defined, otherwise T*
using element_type = T;
using deleter_type = D;
// ...
};
用户无法直接访问其中包含的指针。
unique_ptr<T,D> (§iso.20.7.1.2) cp包含于指针中 | |
unique_ptr up {} | 默认构造函数:cp=nullptr ;constexpr; noexcept |
unique_ptr up {p} | cp=p; 使用默认删除器;constexpr; noexcept |
unique_ptr up {p,del} | cp=p; del 是删除器;noexcept |
unique_ptr up {up2} | 移动构造函数:cp.p=up2.p; up2.p=nullptr; noexcept |
up.˜unique_ptr() | 析构函数:若 cp!=nullptr 则调用 cp 的删除器 |
up=up2 | 移动赋值:up.reset(up2.cp); up2.cp=nullptr; up 获得 up2 的删除器;up的旧对象(如果有)删除;noexcept |
up=nullptr | up.reset(nullptr); 即删除 up 的旧对象(若有) |
bool b {up}; | 转换为 bool: up.cp!=nullptr; noexcept |
x=∗up | x=up.cp;仅针对包含的非数组 |
x=up−>m | x=up.cp−>m; 仅针对包含的非数组 |
x=up[n] | x=up.cp[n]; 仅针对包含的数组 |
x=up.get() | x=up.cp |
del=up.get_deleter() | del 是up的删除器 |
p=up.release() | p=up.cp; up.cp=nullptr |
up.reset(p) | 如果 up.cp!=nullptr 调用 up.cp 的删除器;up.cp=p |
up.reset() | up.cp=pionter{}(可能为 nullptr); 调用删除器获取 up.cp 的旧值 |
up.swap(up2) | 交换 up 和 up2 的值;noexcept |
up==up2 | up.cp==up2.cp |
up<up2 | up.cp<up2.cp |
up!=up2 | !(up==up2) |
up>up2 | up2<up |
up<=up2 | !(up2>up) |
up>=up2 | !(up2<up) |
swap(up,up2) | up.swap(up2) |
注意:unique_ptr 不提供复制构造函数或复制赋值。如果提供,那么“所有权”的含义将很难定义和/或使用。如果您需要复制,请考虑使用 shared_ptr (§34.3.2)。
内置数组可以拥有 unique_ptr。例如:
unique_ptr<int[]> make_sequence(int n)
{
unique_ptr p {new int[n]};
for (int i=0; i<n; ++i)
p[i]=i;
return p;
}
这是作为一项特化提供的:
template<typename T, typename D>
class unique_ptr<T[],D> { // 数组特化 (§iso.20.7.1.3)
// 默认值 D=default_delete<T> 来自一般 unique_ptr
public:
// ... 似单个对象的 unique_ptr , 但用 [] 替代了 *和 -> ...
};
为了避免分片(§17.5.1.4),即使 Base 是 Derived 的公共基类,Derived[] 也不能作为 unique_ptr<Base[]> 的参数。例如:
class Shape {
// ...
};
class Circle : public Base {
// ...
};
unique_ptr<Shape> ps {new Circle{p,20}}; // OK
unique_ptr<Shape[]> pa {new Circle[] {Circle{p,20}, Circle{p2,40}}; // error
我们该如何更好地理解 unique_ptr?unique_ptr 的最佳使用方法是什么?它称为指针(_ptr),我称之为“唯一指针”,但显然它不仅仅是一个普通的指针(否则定义它就毫无意义了)。考虑一个简单的技术示例:
unique_ptr<int> f(unique_ptr<int> p)
{
++∗p;
return p;
}
void f2(const unique_ptr<int>& p)
{
++∗p;
}
void use()
{
unique_ptr<int> p {new int{7}};
p=f(p); //错误 : 没有复制构造函数
p=f(move(p)); //所有权转移
f2(p); //传引用
}
f2() 的函数体比 f() 略短,调用起来也更简单,但我发现 f() 更容易理解。f() 的风格明确地表达了所有权(而 unique_ptr 的使用通常是出于所有权问题)。另请参阅 §7.7.1 中关于非常量引用使用的讨论。总的来说,修改 x 的 f(x) 比不修改 x 的 y=f(x) 更容易出错。
合理的估计是,调用 f2() 比调用 f() 快一到两条机器指令(因为需要在原始 unique_ptr 中放置一个 nullptr ),但这不太可能造成显著影响。在另一方面,与 f() 相比,访问 f2() 中包含的指针需要额外的间接寻址。这在大多数程序中也不太可能造成显著影响,因此,在 f() 和 f2() 的风格之间进行选择时,必须考虑代码质量。
下面是一个简单的示例,该示例使用删除器来保证释放使用 malloc()(§43.5)从 C 程序片段中获得的数据:
extern "C" char∗ get_data(const char∗ data); // 从C 程序片段得到数据
using PtoCF = void(∗)(void∗);
void test()
{
unique_ptr<char,PtoCF> p {get_data("my_data"),free};
// ... use *p ...
} //implicit free(p)
目前,标准库中没有类似于 make_pair() (§34.2.4.1) 和 make_shared() (§34.3.2) 的 make_unique() 函数。不过,它的定义很简单:
template<typename T, typename ... Args>
unique_ptr<T> make_unique(Args&&... args) // 默认删除器版本
{
return unique_ptr<T>{new T{args...}};
}
34.3.2 shared_ptr
shared_ptr 代表共享所有权。它用于两段代码需要访问某些数据,但双方都没有独占所有权(即不负责销毁对象)的情况。shared_ptr 是一种计数指针,当使用计数归零时,指向的对象会被删除。可以把共享指针想象成一个包含两个指针的结构体:一个指向对象,一个指向使用计数:
当使用计数归零时,删除器会删除共享对象。默认删除器是通常的 delete 方法(调用析构函数(如果有)并释放空闲存储空间)。
例如,假设一个通用图中的一个Node,该算法用于添加和删除节点以及节点之间的连接(边)。显然,为了避免资源泄漏,当且仅当没有其他 Node 引用它时,必须删除它。我们可以尝试:
struct Node {
vector<Node∗> edges;
// ...
};
鉴于此,回答诸如“有多少个节点指向这个节点?”之类的问题非常困难,需要添加大量“内部管理”代码。我们可以插入一个垃圾收集器(§34.5),但如果该图只是大型应用程序数据空间的一小部分,这可能会对性能产生负面影响。更糟糕的是,如果容器包含非内存资源,例如线程句柄、文件句柄、锁等,即使是垃圾收集器也可能会泄漏资源。
相反,我们可以使用 shared_ptr:
struct Node {
vector<shared_ptr<Node>> edges;
thread worker;
// ...
};
这里,Node 的析构函数(隐式生成的析构函数就可以)会删除它的边。也就是说,每一个 edge[i] 的析构函数都会被调用,并且如果 edge[i] 是指向它的最后一个指针,则删除指向的 Node(如果有)。
不要仅仅使用 shared_ptr 来将指针从一个所有者传递给另一个所有者;unique_ptr 就是用来传递指针的,而且 unique_ptr 做得更好,成本更低。如果您一直使用计数指针作为工厂函数(§21.2.4)等的返回值,请考虑升级到 unique_ptr,而不是 shared_ptr。
不要为了防止内存泄漏而轻率地用 shared_ptr 替换指针;shared_ptr 并非万能药,也并非没有代价:
• shared_ptr 的循环链接结构可能导致资源泄漏。你需要一些逻辑上的复杂措施来打破这种循环,例如使用 weak_ptr (§34.3.3)。
• 具有共享所有权的对象往往比作用域对象保持“活动”状态的时间更长(因此导致平均资源使用量更高)。
• 多线程环境中的共享指针可能很昂贵(因为需要防止使用计数上的数据竞争)。
• 共享对象的析构函数不会在可预测的时间执行,因此任何共享对象的更新算法/逻辑都比非共享对象更容易出错。例如,在析构函数执行时设置了哪些锁?哪些文件是打开的?通常,哪些对象在(不可预测的)执行点处于“活动”状态并处于适当的状态?
• 如果单个(最后一个)节点维持大型数据结构处于活动状态,则由其删除触发的级联析构函数调用可能会导致严重的“垃圾收集延迟”。这可能不利于实时响应。
shared_ptr 代表共享所有权,它非常有用,甚至必不可少,但共享所有权并非我的理想,而且它总是有代价的(无论你如何表示共享)。如果一个对象有明确的所有者和明确的、可预测的生命周期,那就更好了(更简单)。当有选择的时候:
• 优先使用 unique_ptr 而不是 shared_ptr。
• 优先使用普通作用域对象而不是 unique_ptr 所拥有的堆上对象。
shared_ptr 提供了一组相当常规的操作:
shared_ptr<T> 操作 (§iso.20.7.2.2) cp包含于指针中;uc是使用次数 | |
shared_ptr sp {} | 默认构造函数: cp=nullptr; uc=0; noexcept |
shared_ptr sp {p} | 构造函数: cp=p; uc=1; |
shared_ptr sp {p,del} | 构造函数: cp=p; uc=1;使用删除器 del |
shared_ptr sp {p,del,a} | 构造函数: cp=p; uc=1;使用删除器 del 和分配器 a |
shared_ptr sp {sp2} | 移动和复制构造函数: 移动构造函数移动,然后设置 sp2.cp=nullptr; 复制构造函数复制并设置 ++uc,用于现在共享的 uc |
sp.˜shared_ptr() | 析构函数:−−uc;如果 uc 变为 0 ,则使用删除器删除 cp 指向的对象(默认删除器为 delete) |
sp=sp2 | 复制赋值:++uc 用于现在共享的 uc;noexcept |
sp=move(sp2) | 移动分配:sp2.cp=nullptr 用于现在共享的 uc;noexcept |
bool b {sp}; | 转换为 bool:sp.uc==nullptr; explicit |
sp.reset() | shared_ptr{}.swap(sp);也就是说,sp 包含 pointer{},并且临时 shared_ptr{} 的销毁会减少旧对象的使用计数;noexcept |
sp.reset(p) | shared_ptr{p}.swap(sp); 即 sp.cp=p; uc==1; 临时 shared_ptr 的销毁会减少旧对象的使用计数 |
sp.reset(p,d) | 类似于 sp.reset(p),但使用删除符 d |
sp.reset(p,d,a) | 类似于 sp.reset(p),但使用删除器 d 和分配器 a |
p=sp.get() | p=sp.cp; noexcept |
x=∗sp | x=∗sp.cp; noexcept |
x=sp−>m | x=sp.cp−>m; noexcept |
n=sp.use_count() | n 是使用计数的值(如果 sp.cp==nullptr,则为 0) |
sp.unique() | sp.uc==1? (不检查 sp.cp==nullptr 是否有效) |
x=sp.owner_before(pp) | x 是一个排序函数(严格弱排序;§31.2.2.1)pp 是一个 shared_ptr 或一个weak_ptr |
sp.swap(sp2) | 交换 sp 和 sp2 的值;noexcept |
此外,标准库还提供了一些辅助功能:
shared_ptr<T> 辅助功能(§iso.20.7.2.2.6, §iso.20.7.2.2.7) | |
sp=make_shared(args) | sp 是一个 shared_ptr<T>,用于由参数 args 构造的 T 类型对象;使用 new 分配 |
sp=allocate_shared(a,args) | sp 是一个 shared_ptr<T>,用于由参数 args 构造的 T 类型对象;使用分配器 a 进行分配 |
sp==sp2 | sp.cp==sp2.cp;sp 或 sp2 可能是 nullptr |
sp<sp2 | less<T∗>(sp.cp,sp2.cp); sp 或 sp2 可能是 nullptr |
sp!=sp2 | !(sp=sp2) |
sp>sp2 | sp2<sp |
sp<=sp2 | !(sp>sp2) |
sp>=sp2 | !(sp<sp2) |
swap(sp,sp2) | sp.swap(sp2) |
sp2=static_pointer_cast(sp) | 共享指针的 static_cast:sp2=shared_ptr<T>(static_cast<T∗>(sp.cp)); noexcept |
sp2=dynamic_pointer_cast(sp) | 共享指针的 dynamic_cast: sp2=shared_ptr<T>(dynamic_cast<T∗>(sp.cp)); noexcept |
sp2=const_pointer_cast(sp) | 共享指针的 const_cast: sp2=shared_ptr<T>(const_cast<T∗>(sp.cp)); noexcept |
dp=get_deleter<D>(sp) | 如果 sp 具有类型 D 的删除器,则 ∗dp 是 sp 的删除器;否则,dp==nullptr;noexcept |
os<<sp | 将 sp 写入 ostream os |
例如:
struct S {
int i;
string s;
double d;
// ...
};
auto p = make_shared<S>(1,"Ankh Morpork",4.65);
现在,p 是一个 shared_ptr<S>,指向在空闲存储空间中分配的类型为 S 的对象,包含 {1,string{"Ankh Morpork"},4.65}。
请注意,与 unique_ptr::get_deleter() 不同,shared_ptr 的删除器不是成员函数。
34.3.3 weak_ptr
weak_ptr 指的是由 shared_ptr 管理的对象。要访问该对象,可以使用成员函数 lock() 将 weak_ptr 转换为 shared_ptr。weak_ptr 允许访问由其他人拥有的对象,并且
• 仅当其存在时才需要访问
• 可能随时被(他人)删除
• 必须在最后一次使用后调用其析构函数(通常用于删除非内存资源)
具体来说,我们使用弱指针来打破使用 shared_ptr 管理的数据结构中的循环。
可以将 weak_ptr 想象成一个包含两个指针的结构:一个指向(可能共享的)对象,另一个指向该对象的 shared_ptr 的使用计数结构:
需要“弱使用计数”来保持使用计数结构有效,因为在对象的最后一个 shared_ptr(以及该对象)被销毁后,可能还会有 weak_ptr。
template<typename T>
class weak_ptr {
public:
using element_type = T;
// ...
};
必须将 weak_ptr 转换为 shared_ptr 才能访问其对象,因此它提供的操作相对较少:
weak_ptr<T> (§iso.20.7.2.3) cp 是包含的指针;wuc 是弱使用计数 | |
weak_ptr wp {}; | 默认构造函数:cp=nullptr; constexpr; noexcept |
weak_ptr wp {pp}; | 复制构造函数:cp=pp.cp; ++wuc; pp 是 weak_ptr 或 shared_ptr;noexcept |
wp.˜weak_ptr() | 析构函数:对 ∗cp 无影响;−−wuc |
wp=pp | 复制:减少 wuc 并将 wp 设置为 pp:weak_ptr(pp).swap(wp); pp 是 weak_ptr 或 shared_ptr;noexcept |
wp.swap(wp2) | 交换 wp 和 wp2 的值;noexcept |
wp.reset() | 减少 wuc 并将 wp 设置为 nullptr: weak_ptr{}.swap(wp); noexcept |
n=wp.use_count() | n 是指向 ∗cp 的 shared_ptr 的数量;noexcept |
wp.expired() | 是否有指向 ∗cp 的 shared_ptr?noexcept |
sp=wp.lock() | 为 ∗cp 创建一个新的 shared_ptr;noexcept |
x=wp.owner_before(pp) | x 是排序函数(严格弱排序;§31.2.2.1); pp 是 shared_ptr 或 weak_ptr |
swap(wp,wp2) | wp.swap(wp2); noexcept |
考虑一个旧版“小行星游戏”的实现。所有小行星都归“游戏”所有,但每颗小行星都必须跟踪其邻近的小行星并处理碰撞。碰撞通常会导致一颗或多颗小行星的毁灭。每颗小行星都必须保存其邻近小行星的列表。请注意,位于此类邻近列表上不应使小行星保持“存活”状态(因此使用 shared_ptr 是不合适的)。在另一方面,当另一颗小行星正在观察它时(例如,为了计算碰撞的影响),不能销毁它。显然,必须调用小行星的析构函数来释放资源(例如与图形系统的连接)。我们需要的是一个可能仍然完好的小行星列表,以及一种暂时“抓住”其中一颗小行星的方法。weak_ptr 正是这样做的:
void owner()
{
// ...
vector<shared_ptr<Asteroid>> va(100);
for (int i=0; i<va.siz e(); ++i) {
// ... calculate neighbors for new asteroid ...
va[i].reset(new Asteroid(weak_ptr<Asteroid>(va[neighbor]));
launch(i);
}
// ...
}
显然,我彻底简化了“所有者”的概念,只给每个新小行星一个邻居。关键在于,我们给小行星一个指向该邻居的弱指针 (weak_ptr)。所有者保留一个共享指针 (shared_ptr),用于表示小行星在观察时共享的所有权(否则不共享)。小行星的碰撞计算如下所示:
void collision(weak_ptr<Asteroid> p)
{
if (auto q = p.lock()) { // p.lock 返回指向p的对象的 shared_ptr
// ... that Asteroid still existed: calculate ...
}
else { // Oops: that Asteroid has already been destroyed
p.reset();
}
}
请注意,即使用户决定关闭游戏并删除所有小行星(通过销毁代表所有权的 shared_ptr),每个正在计算碰撞的小行星仍然能够正确完成:在 p.lock() 之后,它持有的 shared_ptr 不会失效。
34.4 内存分配器(Allocators)
STL 容器(§31.4)和 string (第 36 章)是资源句柄,它们获取和释放内存来保存其元素。为此,它们使用分配器。分配器的基本目的是为给定类型提供内存来源,并在不再需要该内存时将其归还到指定位置。因此,基本的分配器函数如下:
p=a.allocate(n); // 为 n 个类型为 T 的对象申请空间
a.deallocate(p,n); // 释放 p 所指向的 n 个类型为 T 的空间
例如:
template<typename T>
struct Simple_alloc { // 使用new[] 和delete[] 来分配和释放字节
using value_type = T;
Simple_alloc() {}
T∗ allocate(size_t n)
{ return reinterpret_cast<T∗>(new char[n∗sizeof(T)]); }
void deallocate(T∗ p, size_t n)
{ delete[] reinterpret_cast<char∗>(p); }
// ...
};
Simple_alloc 恰好是最简单的符合标准的分配器。请注意与 char∗ 的类型转换:allocate() 不会调用构造函数,deallocate() 也不会调用析构函数;它们处理的是内存,而不是类型化对象。
我可以构建自己的分配器来从任意内存区域进行分配:
class Arena {
void∗ p;
int s;
public:
Arena(void∗ pp, int ss); // allocate from p[0..ss-1]
};
template<typename T>
struct My_alloc { // 用一个 Arena 来分配和释放节节
Arena& a;
My_alloc(Arena& aa) : a(aa) { }
My_alloc() {}
// usual allocator stuff
};
一旦创建了 Arenas,就可以在分配的内存中构造对象:
constexpr int sz {100000};
Arena my_arena1{new char[sz],sz};
Arena my_arena2{new char[10∗sz],10∗sz};
vector<int> v0;// 使用默认分配器分配内存
vector<int,My_alloc<int>> v1 {My_alloc<int>{my_arena1}}; // construct in my_arena1
vector<int,My_alloc<int>> v2 {My_alloc<int>{my_arena2}}; // construct in my_arena2
vector<int,Simple_alloc<int>> v3; // construct on free store
通常,使用别名可以减少冗长的内容。例如:
template<typename T>
using Arena_vec = std::vector<T,My_alloc<T>>;
template<typename T>
using Simple_vec = std::vector<T,Simple_alloc<T>>;
My_alloc<int> Alloc2 {my_arena2}; //命名分配器对象
Arena_vec<complex<double>> vcd {{{1,2}, {3,4}}, Alloc2}; // 显式分配
Simple_vec<string> vs {"Sam Vimes", "Fred Colon", "Nobby Nobbs"}; // 默认分配器
仅当容器中的对象实际具有状态时(例如 My_alloc),分配器才会在容器中施加空间开销。这通常是通过依赖空基(empty-base)优化(§28.5)来实现的。
34.4.1 默认分配器
所有标准库容器默认都使用默认分配器,即使用 new 进行分配,使用 delete 进行释放。
template<typename T>
class allocator {
public:
using size_type = size_t;
using difference_type = ptrdiff_t;
using pointer = T∗;
using const_pointer = const T∗;
using reference = T&;
using const_reference = const T&;
using value_type = T;
template<typename U>
struct rebind { using other = allocator<U>; };
allocator() noexcept;
allocator(const allocator&) noexcept;
template<typename U>
allocator(const allocator<U>&) noexcept;
˜allocator();
pointer address(reference x) const noexcept;
const_pointer address(const_reference x) const noexcept;
pointer allocate(size_type n, allocator<void>::const_pointer hint = 0); // allocate n bytes
void deallocate(pointer p, size_type n); // deallocate n bytes
size_type max_siz e() const noexcept;
template<typename U, typename ... Args>
void construct(U∗ p, Args&&... args); // new(p) U{args}
template<typename U>
void destroy(U∗p); //p->˜U()
};
奇怪的 rebind 模板是一个过时的别名。它应该是:
template<typename U>
using other = allocator<U>;
然而,allocator 的定义早于 C++ 支持此类别名。它允许分配器分配任意类型的对象。例如:
using Link_alloc = typename A::template rebind<Link>::other;
如果 A 是一个分配器,那么 rebind<Link>::other 就是 allocator<Link> 的别名。例如:
template<typename T, typename A = allocator<T>>
class list {
private:
class Link { /* ... */ };
using Link_alloc = typename A:: template rebind<Link>::other; // allocator<Link>
Link_alloc a; // link allocator
A alloc; // list allocator
// ...
};
提供了 allocator<T> 的更严格的特化:
template<>
class allocator<void> {
public:
typedef void∗ pointer;
typedef const void∗ const_pointer;
typedef void value_type;
template<typename U> struct rebind { typedef allocator<U> other; };
};
这样可以避免一些特殊情况:只要我们不取消引用 allocator<void> 的指针,我们就可以引用它。
34.4.2 分配器特征(Allocator Traits)
这些分配器使用 allocator_traits 连接在一起。分配器的属性,例如指针类型,可以在其 trait 中找到:allocator_traits<X>::pointer。像往常一样,使用特征技术,这样我就可以为不包含符合分配器要求的成员类型(例如 int)的类型,以及在设计时未考虑任何分配器的类型构建分配器。
基本上,allocator_traits 为常用的类型别名和分配器函数提供了默认值。与默认分配器(§34.4.1)相比,它缺少 address() 函数,并添加了 select_on_container_copy_construction() 函数:
template<typename A> // §iso.20.6.8
struct allocator_traits {
using allocator_type = A;
using value_type = A::value_type;
using pointer = value_type; // trick
using const_pointer = Pointer_traits<pointer>::rebind<const value_type>; // trick
using void_pointer = Pointer_traits<pointer>::rebind<void>; // trick
using const_void_pointer = Pointer_traits<pointer>::rebind<const void>; // trick
using difference_type = Pointer_traits<pointer>::difference_type; // trick
using size_type = Make_unsigned<difference_type>; // trick
using propagate_on_container_copy_assignment = false_type; // trick
using propagate_on_container_move_assignment = false_type; // trick
using propagate_on_container_swap = false_type; // trick
template<typename T> using rebind_alloc = A<T,Args>; // trick
template<typename T> using rebind_traits = Allocator_traits<rebind_alloc<T>>;
static pointer allocate(A& a, size_type n) { return a.allocate(n); } // trick
static pointer allocate(A& a, size_type n, const_void_pointer hint) // trick
{ return a.allocate(n,hint); }
static void deallocate(A& a, pointer p, size_type n) { a.deallocate(p, n); } // trick
template<typename T, typename ... Args>
static void construct(A& a, T∗ p, Args&&... args) // trick
{ ::new (static_cast<void∗>(p)) T(std::forward<Args>(args)...); }
template<typename T>
static void destroy(A& a, T∗ p) { p−>T(); } // trick
static size_type max_size(const A& a) // trick
{ return numeric_limits<siz e_type>::max() }
static A select_on_container_copy_construction(const A& rhs) { return a; } // trick
};
这里的“技巧(trick)”是,如果分配器 A 的等效成员存在,则使用它;否则,使用此处指定的默认值。对于 allocate(n,hint),如果 A 没有 allocate() 接受提示,则会调用 A::allocate(n)。Args 是 A 所需的任何类型参数。
我不喜欢在标准库的定义中使用诡计,但自由使用 enable_if() (§28.4) 可以在 C++ 中实现这一点。
为了使声明可读,我假设了一些类型别名。
34.4.3 指针特征(Pointer Traits)
分配器使用 pointer_traits 来确定指针的属性和指针的代理类型:
template<typename P> // §iso.20.6.3
struct pointer_traits {
using pointer = P;
using element_type = T; // trick
using difference_type = ptrdiff_t; // trick
template<typename U>
using rebind = T∗; // trick
static pointer pointer_to(a); // trick
};
template<typename T>
struct pointer_traits<T∗> {
using pointer = T∗;
using element_type = T;
using difference_type = ptrdiff_t;
template<typename U>
using rebind = U∗;
static pointer pointer_to(x) noexcept { return addressof(x); }
};
此“技巧”与 allocator_traits (§34.4.2) 中使用的相同:如果存在,则使用指针 P 的等效成员;否则,使用此处指定的默认值。要使用 T,模板参数 P 必须是 Ptr<T,args> 模板的第一个参数。
这个规范对 C++ 语言造成了破坏。
34.4.4 作用域分配器(Scoped Allocators)
使用容器和用户定义的分配器时,可能会出现一个相当棘手的问题:元素应该与其容器位于同一个分配作用域中吗?例如,如果您使用 Your_allocator 为 Your_string 分配其元素,而我使用 My_allocator 为 My_vector 分配元素,那么 My_vector<Your_allocator>> 中的字符串元素应该使用哪个分配器?
解决方案是能够告诉容器将哪个分配器传递给元素。关键在于 scoped_allocator 类,它提供了一种机制来跟踪外部分配器(用于元素)和内部分配器(传递给元素供其使用):
template<typename OuterA, typename... InnerA> // §iso.20.12.1
class scoped_allocator_adaptor : public OuterA {
private:
using Tr = allocator_traits<OuterA>;
public:
using outer_allocator_type = OuterA;
using inner_allocator_type = see below;
using value_type = typename Tr::value_type;
using size_type = typename Tr::siz e_type;
using difference_type = typename Tr::difference_type;
using pointer = typename Tr::pointer;
using const_pointer = typename Tr::const_pointer;
using void_pointer = typename Tr::void_pointer;
using const_void_pointer = typename Tr::const_void_pointer;
using propagate_on_container_copy_assignment = /* see §iso.20.12.2 */;
using propagate_on_container_move_assignment = /* see §iso.20.12.2 */;
using propagate_on_container_swap = /* see §iso.20.12.2 */;
// ...
};
我们有四种 string 的 vector 分配方案:
// vector and string use their own (the default) allocator:
using svec0 = vector<string>;
svec0 v0;
// vector (only) uses My_alloc and string uses its own allocator (the default):
using Svec1 = vector<string,My_alloc<string>>;
Svec1 v1 {My_alloc<string>{my_arena1}};
// vector and string use My_alloc (as above):
using Xstring = basic_string<char,char_traits<char>, My_alloc<char>>;
using Svec2 = vector<Xstring,scoped_allocator_adaptor<My_alloc<Xstring>>>;
Svec2 v2 {scoped_allocator_adaptor<My_alloc<Xstring>>{my_arena1}};
// vector uses its own alloctor (the default) and string uses My_alloc:
using Xstring2 = basic_string<char, char_traits<char>, My_alloc<char>>;
using Svec3 = vector<xstring2,scoped_allocator_adaptor<My_alloc<xstring>,My_alloc<char>>>;
Svec3 v3 {scoped_allocator_adaptor<My_alloc<xstring2>,My_alloc<char>>{my_arena1}};
显然,第一个变体 Svec0 将是迄今为止最常见的,但对于内存相关性能受限的系统,其他版本(尤其是 Svec2)可能很重要。增加一些别名可以让代码更具可读性,但幸好这不是你每天都要写的代码。
scoped_allocator_adaptor 的定义有些复杂,但基本上它是一个分配器,很像默认allocator(§34.4.1),它还跟踪其“内部”分配器,以便传递给所包含的容器使用,例如string:
scoped_allocator_adaptor<OuterA,InnerA>(缩写, §iso.20.12.1) | |
rebind<T>::other | 此分配器的一个版本的别名,用于分配类型 T 的对象 |
x=a.inner_allocator() | x 是内部分配器;noexcept |
x=a.outer_allocator() | x 是外部分配器;noexcept |
p=a.allocate(n) | 获取用于 n 个 value_type 对象的空间 |
p=a.allocate(n,hint) | 获取 n 个 value_type 对象的空间 hint 是分配器的一个实现相关的帮助;通常,hint 是一个指向我们希望 ∗p 靠近的对象的指针 |
a.deallocate(p,n) | 释放 p 指向的 n 个 value_type 对象的空间 |
n=a.max_size() | n 是分配的最大元素数量 |
t=a.construct(args) | 从 args 构造一个 value_type:t=new(p) value_type{args} |
a.destroy(p) | 销毁 ∗p:p−>˜value_type() |
34.5 垃圾收集接口(The Garbage Collection Interface)
垃圾回收(自动回收未引用的内存区域)有时被认为是万能的。但事实并非如此。特别是,非纯内存的资源可能会被垃圾回收器泄漏。例如文件句柄、线程句柄和锁。我认为,在常用的防止泄漏技术用尽之后,垃圾回收是最后一种便捷的手段:
[1] 尽可能为应用程序使用具有适当语义的资源句柄。
标准库提供了 string,vector,unordered_map,thread,lock_guard 等。移动语义允许从函数高效地返回这些对象。
[2] 使用 unique_ptr 来保存那些不隐式管理自身资源的对象(例如指针)、需要防止过早删除的对象(因为它们没有合适的析构函数),或者需要以需要特别注意的方式分配的对象(删除器)。
[3] 使用 shared_ptr 来保存需要共享所有权的对象。
如果持续使用这一系列技术,可以确保不会发生泄漏(即,由于不会产生垃圾,因此无需进行垃圾回收)。然而,在大量实际程序中,这些技术(均基于 RAII;§13.3)并未得到一致使用,而且由于它们涉及大量以不同方式构建的代码,因此难以轻松应用。这些“不同方式”通常涉及复杂的指针使用、裸 new 和裸 delete、模糊资源所有权的显式类型转换以及类似的易错低级技术。在这种情况下,垃圾回收器是合适的最后手段。即使它无法处理非内存资源,它也可以回收/再利用内存。千万不要考虑使用在回收时调用的通用“终结器”来尝试处理非内存资源。垃圾回收器有时会显著延长泄漏系统(即使是泄漏非内存资源的系统)的运行时间。例如,对于每晚停机维护的系统,垃圾收集器可能会将资源耗尽的时间从几小时延长到几天。此外,还可以对垃圾收集器进行检测,以查找泄漏源。
值得记住的是,垃圾收集系统可能存在其自身的泄漏变体。例如,如果我们将指向某个对象的指针放入哈希表中,却忘记了它的键,则该对象实际上就泄漏了。同样,无限线程引用的资源也可能永远存在,即使该线程并非注定无限(例如,它可能正在等待永远不会到达的输入)。有时,资源“存活”时间过长对系统造成的危害可能与永久性泄漏一样严重。
从这一基本理念出发,我们可以得出垃圾收集在 C++ 中是可选的。除非明确安装并激活,否则不会调用垃圾收集器。垃圾收集器甚至不是标准 C++ 实现的必需部分,但市面上有优秀的免费和商业垃圾收集器。C++ 定义了垃圾收集器在使用时可以执行的操作,并提供了一个 ABI(应用程序二进制接口)来帮助控制其操作。
指针和生存期的规则以安全派生指针 (§iso.3.7.4.3) 的形式表示。安全派生指针(大致)是“指向通过 new 分配的对象或其子对象的指针”。以下是一些非安全派生指针的示例,也称为伪装指针。让指针指向“其他地方”一段时间:
int∗ p = new int[100];
p+=10;
// ... 收集器可能在此运行...
p −= 10;
∗p = 10; //我们可以确保 int 仍可存于此处吗?
用一个 int 隐藏指针:
int∗ p = new int;
int x = reinterpret_cast<int>(p); // 甚至不可移植
p = nullptr;
// ... 收集器可能在此运行 ...
p = reinterpret_cast<int∗>(x);
∗p = 10; // 我们可以确保 int 仍可存于此处吗?
将指针写入文件并稍后读回:
int∗ p = new int;
cout << p;
p = nullptr;
// ... 收集器可能在此运行 ...
cin >> p;
∗p = 10; // 我们可以确保 int 仍可存于此处吗?
使用“异或技巧”来压缩双向链表:
using Link = pair<Value ,long>;
long xor(Link∗ pre, Link∗ suc)
{
static_assert(sizeof(Link∗)<=sizeof(long),"a long is smaller than a pointer");
return long{pre}ˆlong{suc};
}
void insert_between(Value val, Link∗ pre, Link∗ suc)
{
Link∗ p = new Link{val,xor(pre ,suc)};
pre−>second = xor(xor(pre−>second,suc),p);
suc−>second = xor(p,xor(suc−>second,pre));
}
使用该技巧,不会存储任何未伪装的链接指针。
如果你希望程序行为良好,且普通人易于理解,就不要使用这类技巧——即使你不打算使用垃圾收集器。还有很多更糟糕的技巧,例如将指针的位分散到不同的字中。
伪装指针是有正当理由的(例如,在内存极其受限的应用程序中可以使用异或技巧),但并不像一些程序员想象的那么多。
如果伪装指针的位模式以错误的类型(例如 long 或 char[4])存储在内存中,并且仍然正确对齐,那么细心的垃圾收集器仍然可以发现它。这样的指针称为可追踪的(traceable)。
标准库允许程序员指定无需去哪里找指针(例如,在图像中)以及哪些内存不应该回收,即使收集器找不到指向它的指针(§iso.20.6.4):
void declare_reachable(void∗ p); //p所指向的对象不必收集
template<typename T>
T∗ undeclare_reachable(T∗ p); // 取消 declare_reachable()
void declare_no_pointers(char∗ p, size_t n); // p[0:n) 不保存指针
void undeclare_no_pointers(char∗ p, size_t n); //取消 declare_no_pointers()
C++ 垃圾收集器传统上是保守型的;也就是说,它们不会在内存中移动对象,并且必须假设内存中的每一个字都可能包含指针。保守型垃圾收集器的效率比人们所认为的要高,尤其是在程序不产生大量垃圾的情况下。但 declared_no_pointers() 可以通过安全地排除大量内存空间,使其更加高效。例如,我们可以使用 declared_no_pointers() 来告知垃圾收集器我们的照片在应用程序中的位置,以便让垃圾收集器忽略可能高达数 GB 的非指针数据。
程序员可以查询哪些指针安全和回收规则有效:
enum class pointer_safety {relaxed, preferred, strict };
pointer_safety get_pointer_safety();
标准规定(§iso.3.7.4.3):“非安全派生指针值的指针值是无效指针值,除非引用的完整对象具有动态存储持续时间,并且先前已声明为可访问的……使用无效指针值(包括将其传递给释放函数)的效果是未定义的。”
枚举器意味着:
• ralaxed:安全派生指针和非安全派生指针的处理方式相同(与 C 和 C++98 中相同)。收集所有没有指向安全派生指针或可追踪指针的对象。
• prefered:与ralaxed 类似,但垃圾收集器可能作为泄漏检测器和/或“坏指针”解引用检测器运行。
• strict:安全派生指针和非安全派生指针的处理方式可能不同;也就是说,垃圾收集器可能正在运行,并且会忽略非安全派生的指针。
没有标准的方式来表达你更喜欢哪种方案。你可以考虑考虑实现质量问题或编程环境问题。
34.6 未初始化内存(Uninitialized Memory)
大多数情况下,最好避免使用未初始化的内存。这样做可以简化编程并消除许多类型的错误。然而,在相对罕见的情况下,例如编写内存分配器、实现容器以及直接与硬件打交道时,直接使用未初始化的内存(也称为原内存(raw memory))是必要的。
除了标准 allocator 之外,<memory> 头文件还提供了 fill∗ 系列函数,用于处理未初始化的内存(§32.5.6)。它们都具有一个危险但有时必要的特性,即使用类型名称 T 来引用足以容纳类型 T 对象的空间,而不是指向正确构造的类型 T 对象。这些函数主要面向容器和算法的实现者。例如,reserve() 和 resize() 最容易使用这些函数实现(§13.6)。
34.6.1 临时缓存(Temporary Buffers)
算法通常需要临时空间才能达到可接受的性能。通常,这种临时空间最好在一次操作中分配,但直到实际需要特定位置时才初始化。因此,库提供了一对用于分配和释放未初始化空间的函数:
template<typename T>
pair<T∗,ptrdiff_t> get_temporary_buffer(ptrdiff_t); // 分配内存,非初始化
template<typename T>
void return_temporary_buffer(T∗); //释放内存, 非销毁
get_temporary_buffer<X>(n) 操作尝试为 n 个或更多 X 类型的对象分配空间。如果成功分配内存,它将返回指向第一个未初始化空间的指针,以及可放入该空间的 X 类型对象的数量;否则,该对的第二个值为零。其原理是,系统可能会保留可供快速分配的空间,以便为给定大小的 n 个对象请求空间,从而可能获得大于 n 个对象的空间。然而,也可能获得小于 n 个对象的空间,因此使用 get_temporary_buffer() 的一种方法是乐观地请求大量空间,然后使用恰好可用的空间。
通过 get_temporary_buffer() 获取的缓冲区必须通过调用 return_temporary_buffer() 释放以用于其他用途。正如 get_temporary_buffer() 分配内存而不构造一样,return_temporary_buffer() 释放内存而不销毁。由于 get_temporary_buffer() 是底层函数,并且可能针对管理临时缓存进行了优化,因此不应将其用作 new 或 allocator::allocate() 的替代方案来获取长期存储。
34.6.2 raw_storage_iterator
写入序列的标准算法假定该序列的元素先前已被初始化。也就是说,这些算法使用赋值而不是复制构造进行写入。因此,我们不能将未初始化的内存作为算法的直接目标。这可能很遗憾,因为赋值比初始化的开销要大得多,并且在覆盖之前立即初始化是一种浪费。解决方案是使用来自 <memory> 的 raw_storage_iterator 来初始化而不是赋值:
template<typename Out, typename T>
class raw_storage_iterator : public iterator<output_iterator_tag,void,void,void,void> {
Out p;
public:
explicit raw_storage_iterator(Out pp) : p{pp} { }
raw_storage_iterator& operator∗() { return ∗this; }
raw_storage_iterator& operator=(const T& val)
{
new(&∗p) T{val}; // place val in *p (§11.2.4)
return ∗this;
}
raw_storage_iterator& operator++() {++p; return ∗this; } // pre-increment
raw_storage_iterator operator++(int) // post-increment
{
auto t = ∗this;
++p;
return t;
}
};
raw_storage_iterator 不应用于写入已初始化的数据。这往往会限制其在容器和算法实现的深度范围内的使用。考虑生成一组 string 排列(§32.5.5)用于测试:
void test1()
{
auto pp = get_temporary_buffer<string>(1000); // get uninitialized space
if (pp.second<1000) {
// ... handle allocation failure ...
}
auto p = raw_storage_iterator<string∗,string>(pp.first); // the iterator
generate_n(p,a.siz e(),
[&]{ next_permutation(seed,seed+sizeof(seed)−1); return seed; });
// ...
return_temporary_buffer(p);
}
这是一个略显牵强的例子,因为我认为先为 string 分配默认初始化存储空间,然后再赋值测试字符串并没有什么不妥。而且,它没有使用 RAII(§5.2,§13.3)。
请注意,raw_storage_iterator 没有 == 或 != 运算符,因此请勿尝试使用它来写入 [b:e) 范围。例如,如果 b 和 e 是 raw_storage_iterator,iota(b,e,0) (§40.6) 将不起作用。除非绝对必要,否则请勿处理未初始化的内存。
34.7 建议(Advice)
[1] 当需要一个 constexpr 大小的序列时,请使用数组;§34.2.1。
[2] 优先使用array而不是内置数组;§34.2.1。
[3] 如果需要 N 位,且 N 不一定是内置整数类型的位数,请使用 bitset;§34.2.2。
[4] 避免使用 vector<bool>;§34.2.3。
[5] 使用 pair 时,请考虑使用 make_pair() 进行类型推导;§34.2.4.1。
[6] 使用 tuple 时,请考虑使用 make_tuple() 进行类型推导;§34.2.4.2。
[7] 使用 unique_ptr 表示独占所有权;§34.3.1。
[8] 使用 shared_ptr 表示共享所有权;§34.3.2。
[9] 尽量减少使用 weak_ptr; §34.3.3.
[10] 仅当通常的 new/delete 语义因逻辑或性能原因不足时才使用分配器;§34.4.
[11] 优先使用具有特定语义的资源句柄而非智能指针;§34.5.
[12] 优先使用 unique_ptr 而非 shared_ptr;§34.5.
[13] 优先使用智能指针而非垃圾回收;§34.5.
[14] 制定一致且完整的通用资源管理策略;§34.5.
[15] 垃圾回收对于处理指针使用混乱的程序中的泄漏非常有用;§34.5.
[16] 垃圾回收是可选的;§34.5.
[17] 不要伪装指针(即使不使用垃圾回收);§34.5.
[18] 如果使用垃圾回收,请使用 declared_no_pointers() 让垃圾回收器忽略不能包含指针的数据;§34.5。
[19] 除非绝对必要,否则不要处理未初始化的内存;§34.6。
内容来源:
<<The C++ Programming Language >> 第4版,作者 Bjarne Stroustrup