C++11 标准库扩展 — 通用库
unique_ptr
unique_ptr
(在 <memory>
中定义)提供了严格所有权的语义
- 拥有它所指向的对象
- 不可复制构造,也不可复制赋值,但可移动构造和移动赋值。
- 存储指向对象的指针,并在自身被销毁时(例如,当离开块作用域 (6.7) 时)使用关联的删除器删除该对象。
unique_ptr
的用途包括
- 为动态分配的内存提供异常安全性
- 将动态分配的内存的所有权传递给函数
- 从函数返回动态分配的内存
- 在容器中存储指针
unique_ptr
几乎与使用裸指针一样高效,但具有安全的所有权语义。它是“auto_ptr
应该成为的样子”(但我们无法在 C++98 中实现)。
unique_ptr
是一个仅可移动的类型,因此它严重依赖于右值引用和移动语义。
这是一个传统的 20 世纪异常不安全的代码片段
X* f()
{
X* p = new X;
// do something - maybe throw an exception
return p;
}
一种解决方案是将堆上的对象指针保存在 unique_ptr
中
X* f()
{
unique_ptr<X> p(new X); // or {new X} but not = new X
// do something -- maybe throw an exception
return p.release();
}
现在,如果抛出异常,unique_ptr
将(隐式地)销毁指向的对象。这是基本的 RAII。然而,除非我们确实需要返回一个内置指针,否则通过返回 unique_ptr
可以做得更好
unique_ptr<X> f()
{
unique_ptr<X> p(new X); // or {new X} but not = new X
// do something -- maybe throw an exception
return p; // the ownership is transferred out of f()
}
我们可以这样使用 f
void g()
{
unique_ptr<X> q = f(); // move using move constructor
q->memfct(2); // use q
X x = *q; // copy the object pointed to
// ...
} // q and the object it owns is destroyed on exit
unique_ptr
具有“移动语义”,因此用函数 f()
调用结果的右值初始化 q
只是将所有权转移到 q
中。
unique_ptr
的用途之一是在容器中作为指针,拥有其堆分配的对象,而过去我们可能会使用内置指针,但会遇到异常安全问题(并且无法保证指向的元素被销毁)
vector<unique_ptr<string>> vs { new string{"Doug"}, new string{"Adams"} };
unique_ptr
由一个简单的内置指针表示,与使用内置指针相比,其开销微乎其微。特别是,unique_ptr
不提供任何形式的动态检查。
另请参见
- C++ 草案 20.7.10 节
- Howard E. Hinnant: 适用于 C++03 编译器的 unique_ptr 模拟。
shared_ptr
shared_ptr
用于表示共享所有权;也就是说,当两段代码需要访问某些数据但都没有独占所有权(即负责销毁对象)时。shared_ptr
是一个引用计数指针,当使用计数归零时,指向的对象将被删除。这是一个高度人为的例子
void test()
{
shared_ptr<int> p1(new int); // count is 1
{
shared_ptr<int> p2(p1); // count is 2
{
shared_ptr<int> p3(p1); // count is 3
} // count goes back down to 2
} // count goes back down to 1
} // here the count goes to 0 and the int is deleted.
一个更真实的例子是通用图中的节点指针,其中想要删除节点指针的人不知道是否还有其他人持有该节点的指针。如果一个节点可以持有需要析构函数操作的资源(例如,一个文件句柄,因此当节点被删除时需要关闭文件)。您可以将 shared_ptr
视为可以插入垃圾回收器的东西,但也许您的垃圾不足以使其经济,您的执行环境不允许这样做,或者管理的资源不仅仅是内存(例如,那个文件句柄),因此您需要当最后一个用户离开时确定性地释放,而不是在某个不确定的时间懒惰地完成,并且有序,以便对象的析构函数可以安全地使用其他堆对象。例如
struct Node { // note: a Node may be pointed to from several other nodes.
shared_ptr<Node> left;
shared_ptr<Node> right;
File_handle f;
// ...
};
在这里,Node
的析构函数(隐式生成的析构函数即可)删除其子节点;也就是说,left
和 right
的析构函数被调用。当此节点被销毁时,由于 left
是一个 shared_ptr
,如果 left
是指向它的最后一个指针,则指向的 Node
(如果有)将被 delete
;right
的处理方式类似,f
的析构函数会完成 f
所需的任何操作。
请注意,您不应仅仅为了将指针从一个所有者传递给另一个所有者而使用 shared_ptr
;这是 unique_ptr
的用途,而且 unique_ptr
更便宜、更好。如果您一直将计数指针用作工厂函数等的返回值,请考虑升级到 unique_ptr
而不是 shared_ptr
。
请不要不假思索地用 shared_ptr
替换指针,以防止内存泄漏;shared_ptr
并非万能药,也并非没有成本
shared_ptr
的循环链式结构会导致内存泄漏(您需要一些逻辑复杂性来打破循环,例如使用weak_ptr
),- “共享所有权对象”倾向于比作用域对象“存活”更长时间(从而导致更高的平均资源使用率),
- 在多线程环境中,频繁操作共享指针本身(例如频繁转移所有权,尽管这是一种反模式)可能代价高昂,因为需要避免使用计数上的数据竞争,
- 任何共享对象的更新算法/逻辑比非共享对象更容易出错。
shared_ptr
代表共享所有权,但共享所有权并非总是理想的:默认情况下,如果一个对象具有明确的所有者和明确、可预测的生命周期,则更好。按以下顺序优先选择:栈或成员生命周期(栈变量或按值成员变量);使用 unique_ptr
唯一所有权的堆分配;以及通过 make_shared
共享所有权的堆分配。
另请参见
- C++ 草案:Shared_ptr (20.7.13.3)
- Herb Sutter:《GotW #89: 智能指针》。
- Herb Sutter:《GotW #90: 工厂》。
- Herb Sutter:《GotW #91: 智能指针参数》。
weak_ptr
weak_ptr
用于共享观察,正如 shared_ptr
用于共享所有权一样。weak_ptr
通常被认为是打破使用 shared_ptr
管理的数据结构中循环所必需的,但更一般地说,最好将 weak_ptr
视为指向以下内容的指针
- 您需要访问(仅当存在时),并且
- 可能被删除(由他人),并且
- 必须在最后一次使用后调用其析构函数(通常用于释放非内存资源)
考虑一个旧版“小行星游戏”的实现。所有小行星都由“游戏”拥有,但每颗小行星必须跟踪相邻的小行星并处理碰撞。碰撞通常会导致一颗或多颗小行星的毁灭。每颗小行星必须保留其附近其他小行星的列表。请注意,出现在这样的邻居列表中不应使小行星“存活”(因此 shared_ptr
是不合适的)。另一方面,当另一颗小行星正在查看它时(例如,计算碰撞的影响),小行星不得被销毁。显然,必须调用小行星的析构函数来释放资源(例如与图形系统的连接)。我们需要的只是一个可能仍然完好无损的小行星列表,以及一种在“如果存在的话获取一个”的方法。weak_ptr
正是如此
void owner()
{
// ...
vector<shared_ptr<Asteroid>> va(100);
for (int i=0; i<va.size(); ++i) {
// ... calculate neighbors for new asteroid ...
va[i].reset(new Asteroid(weak_ptr<Asteroid>(va[neighbor]));
launch(i);
}
// ...
}
reset()
是使 shared_ptr
引用新对象的函数。
显然,这个示例代码大大简化了“所有者”,并为每个新的 Asteroid
分配了一个邻居。关键在于我们给了 Asteroid
一个指向该邻居的 weak_ptr
。所有者持有 shared_ptr
来表示共享所有权,只要 Asteroid
正在查看(否则不共享)。Asteroid
的碰撞计算将类似于这样
void collision(weak_ptr<Asteroid> p)
{
if (auto q = p.lock()) { // p.lock returns a shared_ptr to p's object
// ... that Asteroid still existed: calculate ...
}
else {
// ... oops: that Asteroid has already been destroyed: just forget about it (delete the weak_ptr to it ...
}
}
请注意,即使所有者决定关闭游戏并删除所有 Asteroid
(通过销毁表示所有权的 shared_ptr
),每个正在计算碰撞的 Asteroid
仍然会正确完成,因为在 p.lock()
之后,它持有一个 shared_ptr
,该 shared_ptr
将确保 Asteroid
至少在 collision
通过该 shared_ptr
使用它时保持活动状态。
您应该预期 weak_ptr
的使用远比“普通” shared_ptr
的使用少见,而这两种都比 unique_ptr
少见,后者应该最受欢迎,因为它代表了一种更简单(更高效)的所有权概念,因此允许更好的局部推理。
另请参见
- C++ 草案:weak_ptr (20.7.13.3)
垃圾回收 ABI
垃圾回收(自动回收未引用内存区域)在 C++ 中是可选的;也就是说,垃圾回收器不是实现中的强制部分。然而,C++11 提供了垃圾回收器(如果使用)可以做什么的定义,以及一个 ABI(应用程序二进制接口)来帮助控制其操作。
指针和生命周期的规则以“安全派生指针”(3.7.4.3)的形式表达;大致是:“指向由 new 分配的东西或其子对象的指针。”以下是一些“非安全派生指针”的示例,也称为“伪装指针”,或者更直接地说,**在您希望程序表现良好且对普通人可理解时,不要做的事情**
- 让指针暂时指向“其他地方”
int* p = new int;
p+=10;
// ... collector may run here ...
p-=10;
*p = 10; // can we be sure that the int is still there?
- 将指针隐藏在
int
中
int* p = new int;
int x = reinterpret_cast<int>(p); // non-portable
p=0;
// ... collector may run here ...
p = reinterpret_cast<int*>(x);
*p = 10; // can we be sure that the int is still there?
- 还有更多甚至更恶劣的技巧。想想 I/O,想想“将位分散在不同的字中”,……
伪装指针有其正当理由(例如,在内存极度受限的应用程序中使用异或技巧),但没有一些程序员认为的那么多。
程序员可以指定哪里没有指针(例如,在 JPEG 图像中),以及即使收集器找不到指向它的指针也无法回收的内存
void declare_reachable(void* p); // the region of memory starting at p
// (and allocated by some allocator
// operation which remembers its size)
// must not be collected
template<class T> T* undeclare_reachable(T* p);
void declare_no_pointers(char* p, size_t n); // p[0..n] holds no pointers
void undeclare_no_pointers(char* p, size_t n);
程序员可以查询哪些指针安全和回收规则正在生效
enum class pointer_safety {relaxed, preferred, strict };
pointer_safety get_pointer_safety();
3.7.4.3[4]: 如果解引用或释放了非安全派生指针值,且引用的完整对象具有动态存储期且之前未声明可达 (20.7.13.7),则行为未定义。
- relaxed: 安全派生和非安全派生指针被同等对待;像 C 和 C++98,但这并非我的本意——我希望允许 GC,如果用户没有为对象保留有效的指针。
- preferred: 类似于 relaxed;但垃圾收集器可能会作为泄漏检测器和/或“坏指针”解引用检测器运行
- strict: 安全派生和非安全派生指针可能会被不同对待,即垃圾收集器可能正在运行,并将忽略非安全派生指针
没有标准方法说明您喜欢哪种替代方案。这被视为一个“实现质量”和“编程环境”问题。
另请参见
- C++ 草案 3.7.4.3
- C++ 草案 20.7.13.7
- Hans Boehm 的 GC 页面
- Hans Boehm 关于保守 GC 的讨论
- N2527:Hans-J. Boehm 和 Mike Spertus:《垃圾回收和基于可达性泄漏检测的最小支持 (修订版)》(最终提案)
- Michael Spertus 和 Hans J. Boehm:《C++0X 中垃圾回收的现状》。ACM ISMM'09。
tuple
标准库 tuple
(N 元组)是 N
个值的有序序列,其中 N
可以是从 0
到一个较大的实现定义值的常量,在 <tuple>
中定义。您可以将 tuple
视为一个未命名的结构体,其成员具有指定的元素类型。特别是,tuple
的元素是紧凑存储的;tuple
不是链式结构。
tuple
的元素类型可以显式指定或推导(使用 make_tuple()
),并且可以使用 get()
通过(从零开始的)索引访问元素
tuple<string,int> t2("Kylling",123);
auto t = make_tuple(string("Herring"),10, 1.23); // t will be of type tuple<string,int,double>
string s = get<0>(t);
int x = get<1>(t);
double d = get<2>(t);
当我们希望在编译时获得一个异构元素列表但又不想定义一个命名类来保存它们时,可以直接或间接使用元组。例如,std::function
和 std::bind
内部使用元组来保存参数。
最常用的 tuple
是 2 元组;即 pair
。然而,pair
通过 std::pair
(20.3.3 Pairs)在标准库中直接支持。pair
可以用于初始化 tuple
,但反之则不然。
比较运算符(==
、!=
、<
、<=
、>
和 >=
)为可比较元素类型的元组定义。
另请参见
- 标准:20.5.2 类模板
tuple
- [N2087==06-0157] Douglas Gregor:《变参模板简介》。
- Boost::tuple
类型特征
参见
- [N2984==09-0174] B. Dawes, D. Krügler, A. Meredith:《C++0x 的附加类型特征 (修订版 1)》。
function
和 bind
注意:function
长期有用。然而,bind
几乎完全被 C++14 带广义 lambda 捕获的 lambda 取代,但如果您的编译器只支持 C++11 lambda,并且在基本情况下可以更紧凑,它仍然有一些优势。
bind
和 function
标准函数对象在 <functional>
中定义(以及许多其他函数对象);它们用于处理函数和函数参数。bind
用于接受一个函数(或函数对象,或任何您可以使用 (a,b,c)
语法调用的东西)并生成一个函数对象,其中一个或多个参数函数被“绑定”或重新排列。例如
int f(int,char,double);
auto ff = bind(f,_1,'c',1.2); // deduce return type
int x = ff(7); // f(7,'c',1.2);
// equivalent with lambdas
auto ff2 = [](int i){ f(i,'c',1.2); }; // deduce return type
int x2 = ff2(7); // f(7,'c',1.2);
这种参数绑定通常称为“柯里化”。_1
是一个占位符对象,指示当通过 ff
调用 f
时,ff
的第一个参数将放在哪里。第一个参数称为 _1
,第二个为 _2
,依此类推。例如
int f(int,char,double);
auto frev = bind(f,_3,_2,_1); // reverse argument order
int x = frev(1.2,'c',7); // f(7,'c',1.2);
// equivalent with lambdas
auto frev2 = [](double d, char c, int i){ f(i,c,d); }; // reverse argument order
int x2 = frev2(1.2,'c',7); // f(7,'c',1.2);
注意 auto
如何使我们无需指定 bind
结果的类型。
如果要调用的函数是重载的,则无法直接绑定参数。相反,我们必须明确说明要绑定哪个版本的重载函数
int g(int);
double g(double); // g() is overloaded
auto g1 = bind(g,_1); // error: which g()?
auto g2 = bind((double(*)(double))g,_1); // ok (but ugly)
// equivalent with C++11 lambdas, which handle this naturally
auto g3 = [](double d){ g(d); }; // ok in C++11
// both shorter and more powerful with C++14 lambdas
auto g4 = [](auto x){ g(x); }; // ok in C++14, and gives full access to the overload set
bind()
有两种变体:上面所示的一种和一种“遗留”版本,您在其中显式指定返回类型
auto f2 = bind<int>(f,7,'c',_1); // explicit return type
int x = f2(1.2); // f(7,'c',1.2);
第二个版本在 C++98 中是必需的,并且被广泛使用,因为第一个版本(对用户来说最简单)无法在 C++98 中实现。
function
是一种类型,可以容纳几乎任何您可以使用 (a,b,c)
语法调用的值,包括允许参数和返回类型的转换,使其成为一个非常灵活的工具,同时保持严格的类型安全。特别是,bind
的结果可以赋值给 function
。function
使用起来非常简单。例如
function<float (int x, int y)> f; // make a function object
struct int_div { // take something you can call using ()
float operator()(int x, int y) const { return ((float)x)/y; };
};
f = int_div(); // assign
cout << f(5, 3) << endl; // call through the function object
std::accumulate(b,e,1,f); // passes beautifully
成员函数可以被视为自由函数,带有一个额外的“显式 this
”参数
struct X {
int foo(int);
};
function<int (X*, int)> f;
f = &X::foo; // pointer to member
X x;
int v = f(&x, 5); // call X::foo() for x with 5
function<int (int)> ff = std::bind(f,&x,_1); // first argument for f is &x
v=ff(5); // call x.foo(5)
function
对于回调、将操作作为参数传递等非常有用。function
可以看作是 C++98 标准库函数对象 mem_fun_t
、pointer_to_unary_function
等的替代品。类似地,bind()
可以看作是 bind1st()
和 bind2nd()
的替代品。
另请参见
- 标准:20.7.12 函数模板 bind,20.7.16.2 类模板 function
- Herb Sutter:《通用函数指针》。2003 年 8 月。
- Douglas Gregor:《Boost.Function》。
- Boost::bind
正则表达式
待撰写。
同时,请参阅
- 微软
文档 .
时间工具
我们经常需要计时或根据时间做事情。例如,标准库的互斥体和锁提供了线程等待一段时间(一个持续时间)或等待直到给定时间点(一个时间点)的选项。
如果您想知道当前的 time_point
,您可以为三个时钟之一调用 now()
:system_clock
、steady_clock
和 high_resolution_clock
。例如
steady_clock::time_point t = steady_clock::now();
// do something
steady_clock::duration d = steady_clock::now() - t;
// something took d time units
时钟返回一个 time_point
,而 duration
是来自同一时钟的两个 time_point
之间的差值。像往常一样,如果您对细节不感兴趣,auto
是您的朋友
auto t = steady_clock::now();
// do something
auto d = steady_clock::now() - t;
// something took d time units
这里的时间功能旨在有效地支持系统深层的用途;它们不提供方便的功能来帮助您维护您的社交日历。事实上,这些时间功能源于高能物理学的严格需求。为了能够表达所有时间尺度(例如世纪和皮秒),避免单位、打字错误和舍入错误造成的混淆,duration
和 time_point
使用编译时有理数包表示。一个持续时间有两个部分:一个数字时钟“滴答”和一些(一个“周期”)说明一个滴答意味着什么(它是一秒还是一毫秒?);周期是持续时间类型的一部分。以下来自标准头文件 <ratio>
的表格定义了 SI 系统(也称为 MKS 或公制系统)的周期,可能会让您对使用范围有所了解
// convenience SI typedefs:
typedef ratio<1, 1000000000000000000000000> yocto; // conditionally supported
typedef ratio<1, 1000000000000000000000> zepto; // conditionally supported
typedef ratio<1, 1000000000000000000> atto;
typedef ratio<1, 1000000000000000> femto;
typedef ratio<1, 1000000000000> pico;
typedef ratio<1, 1000000000> nano;
typedef ratio<1, 1000000> micro;
typedef ratio<1, 1000> milli;
typedef ratio<1, 100> centi;
typedef ratio<1, 10> deci;
typedef ratio< 10, 1> deca;
typedef ratio< 100, 1> hecto;
typedef ratio< 1000, 1> kilo;
typedef ratio< 1000000, 1> mega;
typedef ratio< 1000000000, 1> giga;
typedef ratio< 1000000000000, 1> tera;
typedef ratio< 1000000000000000, 1> peta;
typedef ratio< 1000000000000000000, 1> exa;
typedef ratio< 1000000000000000000000, 1> zetta; // conditionally supported
typedef ratio<1000000000000000000000000, 1> yotta; // conditionally supported
编译时有理数提供通常的算术运算符(+
、-
、*
和 /
)和比较运算符(==
、!=
、<
、<=
、>
、>=
),适用于 duration
和 time_point
任何有意义的组合(例如,您不能将两个 time_point
相加)。这些操作还会检查溢出和除以零。由于这是一个编译时功能,因此无需担心运行时性能。此外,您可以在 duration
上使用 ++
、--
、+=
、-=
、*=
和 /=
,对于 time_point tp
和 duration d
,可以使用 tp+=d
和 tp-=d
。
以下是一些使用 <chrono>
中定义的标准持续时间类型的示例值
microseconds mms = 12345;
milliseconds ms = 123;
seconds s = 10;
minutes m = 30;
hours h = 34;
auto x = std::chrono::hours(3); // being explicit about namespaces
auto x = hours(2)+minutes(35)+seconds(9); // assuming suitable "using"
您不能将 duration
初始化为分数。例如,不要尝试 2.5 秒;而是使用 2500 毫秒。这是因为持续时间被解释为“滴答”的数量。每个滴答代表持续时间“周期”的一个单位,例如上面定义的 milli
和 kilo
。默认单位是秒;也就是说,对于周期为 1 的持续时间,一个滴答被解释为一秒。我们可以明确 duration
的表示
duration<long> d0 = 5; // seconds (by default)
duration<long,kilo> d1 = 99; // kiloseconds!
duration<long,ratio<1000,1>> d2 = 100; // d1 and d2 have the same type ("kilo" means "*1000")
如果我们要对 duration
做些什么,例如将其打印出来,我们必须给出单位,例如分钟或微秒。例如
auto t = steady_clock::now();
// do something
nanoseconds d = steady_clock::now() - t; // we want the result in nanoseconds
cout << "something took " << d << "nanoseconds\n";
或者,我们可以将 duration
转换为浮点数(以获得舍入结果)
auto t = steady_clock::now();
// do something
auto d = steady_clock::now() - t;
cout << "something took " << duration_cast<double>(d).count() << "seconds\n";
count()
是“滴答”的数量。
另请参见
- 标准:20.9 时间工具 [time]
- Howard E. Hinnant, Walter E. Brown, Jeff Garland, and Marc Paterno: 《A Foundation to Sleep On》。N2661=08-0171。包括“A Brief History of Time”(向斯蒂芬·霍金致歉)。
随机数生成
随机数在许多情况下都很有用,例如测试、游戏、模拟和安全。应用领域的广泛性反映在标准库提供的随机数生成实用程序的多样性中。随机数生成器由两部分组成:(1) 一个生成随机或伪随机值的引擎,以及 (2) 一个将这些值映射到某个范围内的数学分布的分布。分布的示例有 uniform_int_distribution
(其中所有生成的整数都同样可能)和 normal_distribution
(“钟形曲线”),每个都有指定的范围。例如
uniform_int_distribution<int> one_to_six {1,6}; // distribution that maps to the ints 1..6
default_random_engine re {}; // the default engine
要获取一个随机数,您需要用一个引擎调用一个分布
int x = one_to_six(re); // x becomes a value in [1:6]
为了避免在每次调用中传递引擎,我们可以绑定该参数以获得一个无需参数即可调用的函数对象
auto dice {bind(one_to_six,re)}; // make a generator
int x = dice(); // roll the dice: x becomes a value in [1:6]
(由于其对通用性和性能的毫不妥协的关注,一位专家认为标准库随机数组件是“每个随机数库长大后都想成为的样子”。)
如果我们只想要一个简单的用法,比如
int rand_int(int low, int high); // generate a random number from a uniform distribution in [low:high]
那么,我们该如何实现呢?我们必须在 rand_int()
内部放置类似 dice()
的东西
int rand_int(int low, int high)
{
static default_random_engine re {};
using Dist = uniform_int_distribution<int>;
static Dist uid {};
return uid(re, Dist::param_type{low,high});
}
虽然提供该定义需要一些专业知识,但在 C++ 课程的第一周,调用 rand_int()
也是可以管理的。
为了展示一个非平凡的例子,这是一个生成并打印正态分布直方图的程序
#include <iostream>
#include <random>
#include <vector>
std::default_random_engine re; // the default engine
std::normal_distribution<double> nd(31 /* mean */, 8 /* sigma */);
auto norm = std::bind(nd, re);
std::vector<int> mn(64);
int main()
{
for (int i = 0; i<1200; ++i) ++mn[round(norm())]; // generate
for (int i = 0; i<mn.size(); ++i) {
std::cout << i << '\t';
for (int j=0; j<mn[i]; ++j) std::cout << '*';
std::cout << '\n';
}
}
一次执行的结果是
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 *********************************************
27 *********************************************************
28 ***************************************************
29 ******************************************************************
30 **********************************************
31 *********************************************************************
32 **********************************************
33 *************************************************************
34 **************************************************************
35 ***************************************
36 ***********************************************
37 **********************************************
38 *********************************************
39 ********************************
40 ********************************************
41 ***********************
42 **************************
43 ******************************
44 *****************
45 *************
46 *********
47 ********
48 *****
49 *****
50 ****
51 ***
52 ***
53 **
54 *
55 *
56
57 *
58
59
60
61
62
63
另请参见
- 标准 26.5:随机数生成
- Walter E. Brown:《C++11 中的随机数生成》——注意,通常委员会论文不是教程,而且很少能找到由世界级专家(同时也是库设计者)撰写的优秀教程——强烈推荐作为关于
<random>
的首选信息来源
作用域分配器
为了容器对象的紧凑性和简单性,C++98 不要求容器支持带状态的分配器:分配器对象不需要存储在容器对象中。这在 C++11 中仍然是默认设置,但可以使用带状态的分配器,例如一个持有指向分配区域指针的分配器。例如
template<class T> class Simple_alloc { // C++98 style
// no data
// usual allocator stuff
};
class Arena {
void* p;
int s;
public:
Arena(void* pp, int ss);
// allocate from p[0..ss-1]
};
template<class T> struct My_alloc {
Arena& a;
My_alloc(Arena& aa) : a(aa) { }
// usual allocator stuff
};
Arena my_arena1(new char[100000],100000);
Arena my_arena2(new char[1000000],1000000);
vector<int> v0; // allocate using default allocator
vector<int,My_alloc<int>> v1(My_alloc<int>{my_arena1}); // allocate from my_arena1
vector<int,My_alloc<int>> v2(My_alloc<int>{my_arena2}); // allocate from my_arena2
vector<int,Simple_alloc<int>> v3; // allocate using Simple_alloc
通常,冗长性会通过使用 typedef
来缓解。
不能保证默认分配器和 Simple_alloc
在 vector
对象中不占用空间,但库实现中一些优雅的模板元编程可以确保这一点。因此,只有当分配器对象实际具有状态(如 My_alloc
)时,使用分配器类型才会带来空间开销。
在使用容器和用户定义分配器时,可能会出现一个相当隐蔽的问题:元素是否应该与容器位于同一分配区域?例如,如果您使用 Your_alloc
为 Your_string
分配其元素,而其他人使用 My_alloc
为 My_vector
分配元素,那么在 My_vector<Your_alloc>>
中的 string
元素应该使用哪个分配器?解决方案是能够告诉容器将哪个分配器传递给元素。例如,假设有一个分配器 My_alloc
,您想要一个 vector<string>
,它同时使用 My_alloc
分配 vector
元素和 string
元素,首先,您必须创建一个接受 My_alloc
对象的字符串版本
using xstring = basic_string<char, char_traits<char>, My_alloc<char>>; // a string with my allocator
然后,您必须创建一个 vector
版本,它接受这些字符串,接受一个 My_alloc
对象,并将该对象传递给字符串
using svec = vector<xstring,scoped_allocator_adaptor<My_alloc<xstring>>>;
最后,我们可以创建一个 My_alloc<xstring>
类型的分配器
svec v(svec::allocator_type(My_alloc<xstring>{my_arena1}));
现在 svec
是一个字符串向量,使用 My_alloc
来为字符串分配内存。新颖之处在于标准库的“适配器”(“包装类型”)scoped_allocator_adaptor
用于指示字符串也应使用 My_alloc
。请注意,适配器可以(简单地)将 My_alloc<xstring>
转换为字符串所需的 My_alloc<char>
。
所以,我们有四种选择
// 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 (the default) allocator:
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 My_alloc and string uses My_string_alloc:
using xstring2 = basic_string<char, char_traits<char>, My_string_alloc<char>>;
using svec3 = vector<xstring2,scoped_allocator_adaptor<My_alloc<xstring>, My_string_alloc<char>>>;
svec3 v3(scoped_allocator_adaptor<My_alloc<xstring2>, My_string_alloc<char>>{my_arena1,my_string_arena});
显然,第一种变体 svec0
将是最常见的,但对于具有严格内存相关性能限制的系统,其他版本(尤其是 svec2
)可能很重要。一些 typedef
会使代码更具可读性,但它不是您每天都必须编写的东西。scoped_allocator_adaptor2
是 scoped_allocator_adaptor
的一个变体,适用于两个非默认分配器不同的情况。
另请参见
- 标准:20.8.5 作用域分配器适配器 [allocator.adaptor]
- Pablo Halpern:《作用域分配器模型 (修订版 2)》。N2554=08-0064。