标准 C++ 库中的新功能
正如我所提到的,功能包还包括作为 TR1 的一部分添加到标准 C++
库中的大量附加功能。其中包括支持引用计数的智能指针、多态函数包装、基于哈希表的容器、正则表达式等等。下面我将介绍其中的一些新 TR1 功能。
多态函数对象
在许多应用程序中都有一个至关重要的功能,就是能够将函数作为一个值加以引用并能够将其作为参数来传递或存储起来以备今后使用。此概念可用于实现各种常见的构造,包括回调函数、事件处理程序和异步编程功能等。但是,函数在
C++ 中非常难于处理。函数设计的驱动力主要源自与 C
的兼容性的要求以及对优良性能的要求。尽管实现了这些目标,但在将函数视为可存储、可传递并最终能够异步调用的对象方面,却并未能使其变得简单一些。让我们来看一看
C++ 中常见的一些类似函数的构造。
首先,是一个不错的古老非成员函数:
int Add(int x, int y)
{
return x + y;
}
正常情况下,可通过如下方法调用它:
int result = Add(4, 5);
ASSERT(4 + 5 == result);
另一个常见的类似函数的构造是函数对象(即算符):
class AddFunctor
{
public:
int operator()(int x, int y) const
{
return x + y;
}
};
由于它实现调用运算符,因此可像使用函数一样来使用函数对象:
AddFunctor fo;
int result = fo(4, 5);
ASSERT(4 + 5 == result);
接下来是非静态成员函数:
class Adder
{
public:
int Add(int x, int y) const
{
return x + y;
}
};
当然,调用成员函数需要使用对象:
Adder adder;
int result = adder.Add(4, 5);
ASSERT(4 + 5 == result);
到目前为止一切顺利。现在,假设您需要将这些类似函数的构造存储起来以备今后使用。可按如下方式定义一个能存储指向非成员函数的指针的类型:
typedef int (*FunctionPointerType)(int x, int y);
也可将函数指针作为函数来使用:
FunctionPointerType fp = &Add;
int result = fp(4, 5);
ASSERT(4 + 5 == result);
尽管函数对象也可以存储下来,但它无法与函数指针一起以多态形式存储。
成员函数可存储在 pointer-to-member-function 中:
Adder adder;
typedef int (Adder::*MemberFunctionPointerType)(int x, int y);
MemberFunctionPointerType mfp = &Adder::Add;
但是,pointer-to-member-function 类型与
pointer-to-non-member-function
类型不兼容,因此无法与其非成员函数竞争者一起以多态形式存储。即使可以,成员函数仍需要一个对象来提供成员函数调用的上下文:
int result = (adder.*mfp)(4, 5);
ASSERT(4 + 5 == result);
我想我不必再做解释您也应该明白我的意思了。幸运的是,新的 tr1::function
类模板提供了一个解决方案。tr1::function
类模板为在其模板参数中定义的函数类型保存着一个可调用对象。接下来,我将使用非成员函数对其进行初始化:
function<int (int x, int y)> f = &Add;
int result = f(4, 5);
ASSERT(4 + 5 == result);
使用函数对象来初始化也一样轻松:
function<int (int x, int y)> f = AddFunctor();
您甚至还可以使用新的函数绑定功能通过成员函数对其进行初始化:
function<int (int x, int y)> f = bind(&Adder::Add, &adder, _1, _2);
有关 bind
函数的内容我会在稍后做介绍,但在这里您需要了解的就是现在可将单个函数包装绑定到非成员函数、函数对象甚至成员函数中。可将其存储下来并在今后随时调用,所有这一切都是以多态形式执行的。
函数包装也是可以重新绑定的,并且可以像普通的函数指针一样设置为空值:
function<int (int x, int y)> f;
ASSERT(0 == f);
f = &Add;
ASSERT(0 != f);
f = bind(&Adder::Add, &adder, _1, _2);
bind 函数模板的功能要比标准 C++ 库中的函数对象适配器强大得多——尤其是
std::bind1st() 和 std::bind2nd()。在这个示例中,bind
的第一个参数是成员函数的地址。第二个参数是对象的地址,届时将在此对象中调用成员。此示例中的最后两个参数定义了调用函数时将要解析的占位符。
当然,bind 并不仅限于成员函数。您可通过绑定标准 C++ 库的 multiplies
函数对象来创建一个平方函数,利用此函数可生成一个能得出参数平方结果的单参数函数:
function<int (int)> square = bind(multiplies<int>(), _1, _1);
int result = square(3);
ASSERT(9 == result);
请注意,tr1::function 类模板非常适合与标准 C++
库算法一起使用。给定一个整数容器,就可以使用成员函数生成所有值的总和,如下所示:
function<int (int x, int y)> f = // initialize
int result = accumulate(numbers.begin(),
numbers.end(),
0, // initial value
f);
请记住,tr1::function
类模板可能会禁止编译器优化(如内联),但如果您只是直接使用函数指针或函数对象则可能不会出现这一问题。因此,请仅在必要时才使用 tr1::function
类模板,例如当使用可能会被重复调用的累积算法时。如果可能,应直接将函数指针、成员函数指针(使用 TR1 的 mem_fn 改写过的)以及函数对象(如 bind
所返回的)传递给标准 C++ 库算法和其他模板化的算法。
让我们接着往下看。接下来还有个更有趣的问题。假设有个 Surface 类代表一些绘图表面,还有个
Shape 类,它可以将其自身绘制到表面上:
class Surface
{
//...
};
class Shape
{
public:
void Draw(Surface& surface) const;
};
现在考虑一下怎样才能够将容器中的每个形状都绘制到给定表面上。您可能会考虑使用 for_each
算法,如下所示:
Surface surface = // initialize
for_each(shapes.begin(),
shapes.end(),
bind(&Shape::Draw, _1, surface)); // wrong
在这里,我打算利用 bind 函数模板来针对形状容器的每个元素调用成员函数,从而将表面作为参数绑定到
Draw 成员函数。但遗憾的是,这要取决于 Surface
的定义方式,有时可能无法按预期的那样运行或编译。之所以出现这个问题,是因为当您实际需要的是一个引用时,bind 函数模板却试图生成表面副本。值得庆幸的是,TR1
还引入了 reference_wrapper 类模板,它允许您将引用视为一个可随意复制的值。由于类型推断功能的存在,ref 和 cref 函数模板可简化
reference_wrapper 对象的创建过程。
借助于 reference_wrapper,for_each
算法现在可以简单有效地将形状成功绘制到表面上:
for_each(shapes.begin(),
shapes.end(),
bind(&Shape::Draw, _1, ref(surface)));
正如您所设想的,对于新的函数包装、绑定功能和引用包装,可以通过多种方式来组合它们,从而灵活地解决各种问题。
智能指针
智能指针对 C++ 开发人员而言是不可或缺的工具。我通常使用 ATL 的 CComPtr 来处理
COM 接口指针,使用标准 C++ 库的 auto_ptr 来处理原始 C++ 指针。后者非常适合需要动态创建 C++ 对象并能够确保当 auto_ptr
对象超出范围时可以将对象安全删除的情形。
智能指针像 auto_ptr
一样都非常有用,但它只能安全地用在少数情形下。这主要是因为它所实现的所有权转移语义。即,如果复制或分配 auto_ptr 对象,则基础资源的所有权将被转移,原始
auto_ptr
对象将失去它。只有当您对资源分配拥有精细的控制权时它才会有明显作用,但很多情况下您可能需要共享对象,这时实现共享所有权语义的智能指针将会非常有用。更为重要的是,auto_ptr
无法与标准 C++ 库容器一起使用。
TR1 引入了两个新的智能指针,它们协同工作来提供多种用途。shared_ptr 类模板的工作方式与
auto_ptr 十分相似,但它不能转移资源的所有权,它只是增加资源的引用计数。如果用来保存对象引用信息的最后一个 shared_ptr
对象被破坏或重置,资源将被自动删除。通过 weak_ptr 类模板与 shared_ptr
的协同工作,调用方可以在不影响引用计数的情况下引用资源。如果在对象模型中有循环关系或打算实现缓存服务,则这将非常有用。它还非常适合与标准 C++
库容器一起使用!
作为对比,请看一看以下的 auto_ptr 用法:
auto_ptr<int> ap(new int(123));
ASSERT(0 != ap.get());
// transfer ownership from ap to ap2
auto_ptr<int> ap2(ap);
ASSERT(0 != ap2.get());
ASSERT(0 == ap.get());
auto_ptr 复制构造函数将所有权从 ap 传输到 ap2。shared_ptr
的行为同样是可预测的:
shared_ptr<int> sp(new int(123));
ASSERT(0 != sp);
// increase reference count of shared object
shared_ptr<int> sp2(sp);
ASSERT(0 != sp2);
ASSERT(0 != sp);
从内部来说,引用相同资源的所有 shared_ptr
对象都共享一个控制块,此控制块将跟踪共同拥有资源的 shared_ptr 对象的数量以及引用此资源的 weak_ptr 对象的数量。稍后我将展示如何使用
weak_ptr 类模板。
与 auto_ptr 类似的成员函数由 shared_ptr
提供。其中包括解引用操作符和箭头操作符、用来替换资源的 reset 成员函数以及返回资源地址的 get
成员函数。此外还提供一些特有的成员函数(其中包括一个恰好也以 unique 命名的函数)。unique 成员函数将测试 shared_ptr
对象是否为保存着资源引用信息的唯一智能指针。示例如下:
shared_ptr<int> sp(new int(123));
ASSERT(sp.unique());
shared_ptr<int> sp2(sp);
ASSERT(!sp.unique());
ASSERT(!sp2.unique());
也可以使用 use_count 成员函数来获取拥有资源的 shared_ptr
对象的数量:
shared_ptr<int> sp;
ASSERT(0 == sp.use_count());
sp.reset(new int(123));
ASSERT(1 == sp.use_count());
shared_ptr<int> sp2(sp);
ASSERT(2 == sp.use_count());
ASSERT(2 == sp2.use_count());
但是,应将 use_count
的用途仅限于进行调试,因为无法保证它在所有实现中都是一个恒定的时间操作。请注意,可借助提供的操作符 unspecified-bool-type 来确定
shared_ptr 是否拥有资源,并且可使用 unique 函数来确定 shared_ptr 是否为某个资源的唯一拥有者。
weak_ptr 类模板存储着对 shared_ptr 对象所拥有的资源的弱引用。如果拥有资源的所有
shared_ptr 对象都被破坏或重置,资源将被删除,无论是否有 weak_ptr 对象正在引用它。为确保不使用仅被 weak_ptr
对象引用的资源,weak_ptr 类模板不会提供熟悉的 get
成员函数来返回资源的地址或成员访问操作符。相反,首先必须将弱引用转换为强引用才能访问资源。lock 成员函数提供了此功能,如
图
8 所示。
Figure 8 转换成强引用
Surface surface;
shared_ptr<Shape> sp(new Shape);
ASSERT(1 == sp.use_count());
weak_ptr<Shape> wp(sp);
ASSERT(1 == sp.use_count()); // still 1
// arbitrary application logic...
if (shared_ptr<Shape> sp2 = wp.lock())
{
sp2->Draw(surface);
}
如果资源中途被释放,weak_ptr 对象的 lock 成员函数会返回一个并不拥有资源的
shared_ptr 对象。可以想象,shared_ptr 和 weak_ptr 必将会极大地简化许多应用程序中的资源管理工作。
毋庸置疑,Visual C++ 2008 功能包是一个备受欢迎的 Visual C++
库升级程序,肯定迟早会派上用场!值得研究的内容不胜枚举,但我希望通过本文的介绍能激起您的兴趣,使您能够花些时间亲自深入研究一下。