当前位置: 首页 > news >正文

C++编程语言:标准库:内存和资源管理(Bjarne Stroustrup)

第 34 章  内存和资源管理(Memory and Resources)

目录

34.1  引言

34.2  准容器(“Almost Containers”)

34.2.1  array

34.2.2  bitset

34.2.2.1  构造函数

34.2.2.2  bitset 操作

34.2.3  vector

34.2.4  元组(Tuples)

34.2.4.1  pair

34.2.4.2  tuple

34.3  资源管理指针(Resource Management Pointers)

34.3.1  unique_ptr

34.3.2  shared_ptr

34.3.3  weak_ptr

34.4  内存分配器(Allocators)

34.4.1  默认分配器

34.4.2  分配器特征(Allocator Traits)

34.4.3  指针特征(Pointer Traits)

34.4.4  作用域分配器(Scoped Allocators)

34.5  垃圾收集接口(The Garbage Collection Interface)

34.6  未初始化内存(Uninitialized Memory)

34.6.1  临时缓存(Temporary Buffers)

34.6.2  raw_storage_iterator

34.7  建议(Advice)


34.1  引言

    STL(第31章,第32章,第33章)是用于管理和操作数据的标准库工具中结构化程度最高、通用性最强的部分。本章介绍一些更专业或处理原始内存(而非类型化对象)的工具。

34.2  准容器(“Almost Containers”)

    标准库提供了一些与 STL 框架并不完美契合的容器(§31.4,§32.2,§33.1)。例如内置数组,arraystring。我有时会将它们称为“准容器”(§31.4),但这不太公平:它们可以保存元素,因此它们是容器,但每一个容器都存在一些限制或附加功能,这使得它们在 STL 的上下文中显得有些不合适。单独描述它们也简化了 STL 的描述。

“准容器”

T[N]

内置数组:一个固定大小、连续分配的序列,包含 N 个元素,类型为 T;隐式转换为 T∗

array<T,N>

一个固定大小、连续分配的序列,包含 N 个元素,类型为 T;类似内置数组,但解决了大多数内置数组存在的问题

bitset<N>

N 位固定大小序列

vector<bool>

vector特化形式紧凑存储的位序列

pair<T,U>

两个类型为 TU 的元素

tuple<T...>

任意数量、任意类型的元素的序列

basic_string<C>

C 类型的字符序列;提供字符串操作

valarray<T>

T 类型的数值数组;提供数字运算

为什么标准库提供这么多容器?它们满足一些常见但又不同(通常重叠)的需求。如果标准库不提供这些容器,许多人就不得不自己设计和实现。例如:

pairtuple异构的;所有其他容器都是同构的(所有元素均为同一类型)。

array, vectortuple 的元素是连续分配的;forward_list map 是链式结构。

bitsetvector<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> // NT的数组 (§iso.23.3.2)

struct array {

/*

类似于vector的类型和操作 (§31.4),

只是那些改变容器大小,构造函数和赋值函数的操作除外

*/

void fill(const T& v);// 赋值vN个副本

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的元素数量在编译时已知,因此arraysize() 是一个 constexpr 函数。

    array没有复制参数值的构造函数(vector有;§31.3.2)。相反,它提供了一个 fill() 操作:

void f()

{

array<int,8> aa; // 未初始化

aa.fill(99); // 赋值8 99 的副本

// ...

}

由于array不遵循“元素句柄”模型,因此 swap() 必须实际交换元素,因此交换两个 array<T,N> 会将 swap() 应用于 NT 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};

sn[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

zC 类型字符,用于表示位置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写入osos 是一个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)

CTr 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<8sizeof(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

}

    这将从左到右打印表示为 10 的位,最高有效位放在最左边,因此参数 123 将给出输出

00000000000000000000000001111011

00000000000000000000000001111011

00000000000000000000000001111011

00000000000000000000000001111011

对于这个例子来说,直接使用 bitset 输出运算符更简单:

void binary2(int i)

{

bitset<8sizeof(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 初始化为 xp.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 的元素上的相应操作成立,则复制或移动操作成立。

       元素 firstsecond 是可以直接读写的成员。例如:

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 作为其 firstsecond 参数列表构造一个 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 必须为 01

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 是一个或多个tuplet 按顺序包含 args 中的tuple成员

tuple_size<T>::value

tuple T 的元素数量

tuple_elements<N,T>::type

tuple T 的第N个元素类型

get<N>(t)

tuple t 的第N个元素的引用

t==t2

tt2的所有元素都相等吗?如果相等,则tt2一定有相同的元素数量

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)

交换 upup2 的值;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),即使 BaseDerived 的公共基类,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_ptrunique_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 中关于非常量引用使用的讨论。总的来说,修改 xf(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};

转换为 boolsp.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.cpsp 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_castsp2=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 的删除器,则 dpsp 的删除器;否则,dp==nullptrnoexcept

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_ptrweak_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 设置为 ppweak_ptr(pp).swap(wp)

pp weak_ptr shared_ptr;noexcept

wp.swap(wp2)

交换 wpwp2 的值;noexcept

wp.reset()

减少 wuc 并将 wp 设置为 nullptr:

weak_ptr{}.swap(wp); noexcept

n=wp.use_count()

n 是指向 cpshared_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[nsizeof(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[10sz],10sz};

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(Up); //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)

获取用于 nvalue_type 对象的空间

p=a.allocate(n,hint)

获取 n value_type 对象的空间 hint 是分配器的一个实现相关的帮助;通常,hint 是一个指向我们希望 p 靠近的对象的指针

a.deallocate(p,n)

释放 p 指向的 nvalue_type 对象的空间

n=a.max_size()

n 是分配的最大元素数量

t=a.construct(args)

args 构造一个 value_typet=new(p) value_type{args}

a.destroy(p)

销毁 pp>˜value_type()

34.5  垃圾收集接口(The Garbage Collection Interface)

    垃圾回收(自动回收未引用的内存区域)有时被认为是万能的。但事实并非如此。特别是,非纯内存的资源可能会被垃圾回收器泄漏。例如文件句柄、线程句柄和锁。我认为,在常用的防止泄漏技术用尽之后,垃圾回收是最后一种便捷的手段:

[1] 尽可能为应用程序使用具有适当语义的资源句柄。

标准库提供了 stringvectorunordered_mapthreadlock_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));

}

使用该技巧,不会存储任何未伪装的链接指针。

    如果你希望程序行为良好,且普通人易于理解,就不要使用这类技巧——即使你不打算使用垃圾收集器。还有很多更糟糕的技巧,例如将指针的位分散到不同的字中。

伪装指针是有正当理由的(例如,在内存极其受限的应用程序中可以使用异或技巧),但并不像一些程序员想象的那么多。

如果伪装指针的位模式以错误的类型(例如 longchar[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 eraw_storage_iteratoriota(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

 

 

 

 

 

 

http://www.dtcms.com/a/278539.html

相关文章:

  • 对偶原理与蕴含定理
  • UART寄存器介绍
  • 解决安装 make 时 “configure: error: C compiler cannot create executables” 报错
  • 用于监测线性基础设施的分布式声学传感:现状与趋势
  • week3
  • 阿里云ODPS多模态数据处理实战:MaxFrame的分布式AI数据管道构建
  • ISO 15765-2TP传输协议
  • 迁移学习之图像预训练理解
  • 【双链表】【数组】
  • ubuntu(22.04)系统上安装 MuJoCo
  • 计算机网络(基础概念)
  • 网络协议和基础通信原理
  • qt-- 编译工具-Cmake的使用
  • 一文读懂循环神经网络(RNN)—语言模型+读取长序列数据(2)
  • Python----NLP自然语言处理(NLP自然语言处理解释,NLP的发展历程)
  • QT——文件操作类 QFile和QTextStream
  • 【同等学力-计算机-真题解析】离散数学-图论(握手定理、欧拉公式)
  • ARMv8.1原子操作指令(ll_sc/lse)
  • #Paper Reading# Apple Intelligence Foundation Language Models
  • 【Linux网络】:HTTP(应用层协议)
  • 深入解析 Transformer:开启自然语言处理新时代的革命性模型
  • uni-app在安卓设备上获取 (WIFI 【和】以太网) ip 和 MAC
  • 游戏框架笔记
  • SAP ERP与微软ERP dynamics对比,两款云ERP产品有什么区别?
  • [个人笔记] WSL 完整使用指南及 Claude Code 配置记录
  • 019_工具集成与外部API调用
  • 【HarmonyOS】元服务概念详解
  • ubuntu系统在线安装postgres
  • 【视频格式转换】.264格式转为mp4格式
  • React Three Fiber 实现 3D 模型视图切换、显隐边框、显隐坐标轴