考虑写出一个不抛异常的swap函数——条款25

        swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程(exception-safe programing,见条款29)的脊柱,以及用来处理自我赋值可能性(见条款11)的一个常见机制。由于swap如此有用,适当的实现很重要。

        所谓swap(置换)两对象值,意思是将两对象的值彼此赋予对方。缺省情况下swap动作可由标准程序库提供的swap算法完成。其典型实现:

namespace std {
	template<typename T>      // std::swapd的典型实现;
	void swap(T& a, T& b)      // 置换a和b的值
	{
		T temp(a);
		a = b;
		b = temp(b);
	}
}

        只要类型T支持copying(通过copy构造函数和copy assignment操作符完成),缺省的swap实现代码就会帮你置换类型为T的对象,你不需要为此另外再做任何工作。

        这个缺省的swap实现涉及三个对象复制:a复制到temp,b复制到a,以及temp复制到b。但是对某些类型而言,这些复制动作无一必要;对它们而言swap缺省行为等于是把高速铁路铺设在慢速小巷弄内。

        其中最主要的就是“以指针指向一个对象,内含真正数据”的那种类型。这种设计常见表现形式是所谓“pimpl手法”(pimpl是“pointer to implementation”的缩写,见条款31)。如果以这种手法设计Widget class,看起来会像这样:

class WidgetImpl {            // 针对Widget数据而设计的class
public:         
	...                       // 细节不重要
private:
	int a, b, c;              // 可能有许多数据
	std::vector<double> v;    // 意味复制时间很长
	...
};

class Widget {                              // 这个class使用pimpl手法
public:
	Widget(const Widget& rhs);
	Widget& operator=(const Widget& rhs)    // 复制Widget时,令它复制其WidgetImpl对象。
	{
		...                                 // 关于operator=的一般性实现
		*pImpl = *(rhs.pImpl);              // 细节,见条款10,11和12
		...
	}
	...
private:
	WidgetImpl* pImpl;                      // 指针,所指对象内含Widget数据
};

        一旦要置换两个Widget对象值,我们唯一需要做的就是置换其pImpl指针,但缺省的swap算法不知道这一点。它不只复制是三个Widgets,还复制三个WidgetImpl对象。非常缺乏效率!

        我们希望能够告诉std::swap:当Widgets被置换时真正该做的是置换其内部的pImpl指针。确切实践这个思路的一个做法是:将std::swap针对Widget特化。下面是基本构想,但目前这个形式无法通过编译:

namespace std {
	template<>      // 这是std::swapd针对“T是Widget”的特化版本;
	void swap<Widget>(Widget& a, Widget& b)      // 目前无法通过编译
	{ 
		swap(a.pImpl, b.pImpl);                  // 置换Widgets时只置换它们的pImpl指针就好
	}
}

        这个函数一开始的“template<>”表示它是std::swap的一个全特化(total template specialization)版本,函数名称之后的“<Widget>”表示这一特化版本针对“T是Widget”而设计。换句话说当一般性的swap template施行于Widgets身上便会启用这个版本。通常我们不被允许改变std命名空间内的任何东西,但可以为标准templates(如swap)制造特化版本,使它专属于我们自己的classes(例如Widget)。以上作为正是如此。

        但是一如稍早我说,这个函数无法通过编译。因为它企图访问a和b内的pImpl指针,而那却是private。我们可以将这个特化版本声明为friend,但和以往的规矩不太一样:我们令Widget声明为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用该成员函数:

class Widget {                              // 与前相同,唯一差别是增加swap函数
public:
	...
	void swap(Widget& other)
	{
		using std::swap;                    // 这个声明之所以必要,稍后解释。
		swap(pImpl, other.pImpl);           // 若要置换Widgets就置换其pImpl指针。
	}
	...
};
namespace std {
	template<>      // 修订后的std::swap特化版本
	void swap<Widget>(Widget& a, Widget& b)     
	{ 
		swap(a.pImpl, b.pImpl);                  // 若要置换Widgets,调用其swap成员函数
	}
}

       这种做法不只能通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swap特化版本(用以调用前者)。

        然而假设Widget和WidgetImpl都是class templates而非classes,也许我们可以试试将WidgetImpl内的数据类型加以参数化:

template<typename T>
class WidgetImpl { ... };

template<typename T>
class Widget { ... };

        在Widget内(以及WidgetImpl内,如果需要的话)放个swap成员函数就像以往一样简单,但我们却在特化std::swap时遇上乱流。我们想写成这样:

namespace std {
	template<typename T>
	void swap< Widget<T> >(Widget<T>& a, Widget<T>& b) {   // 错误!不合法!
		a.swap(b);
	}
}

        看起来合情合理,却不合法。我们企图偏特化(partially specialize)一个function template(std::swap),但C++只允许对class templates偏特化,在function templates身上偏特化是行不通的。

namespace std {
	template<typename T>                      // std::swap的一个重载版本
	void swap(Widget<T>& a, Widget<T>& b) {   // 注意“swap”之后没有“<...>”, 但即使这样也不合法。
		a.swap(b);
	}
}

        一般而言,重载function templates没有问题,但std是个特殊的命名空间,其管理规则也比较特殊。客户可用全特化std内的templates,但不可以添加新的templates(或classes或functions或其它任何东西)到std里头。

         那该如何是好?毕竟我们总是需要一个办法让其他人调用swap时能够取得我们提供的较高效的template特定版本。答案很简单,我们还是声明一个non-member swap让它调用member swap,但不再将那个non-member声明为std::swap的特化版本或重载版本。假设Widget的所有相关机能都被置于命名空间WidgetStuff内,整个结果看起来便像这样:

namespace WidgetStuff {
	...                                       // 模板化的WidgetImpl等等。
	template<typename T>                      // 同前,内含swap成员函数。
	class WidgetImpl { ... };
	
	...
	template<typename T>                      // non-member swap函数;
	void swap(Widget<T>& a, Widget<T>& b) {   // 这里并不属于std命名空间。
		a.swap(b);
	}
}

        这个做法对classes和class templates都行得通,所以似乎我们应该任何时候都使用它。

        此刻,我们已经讨论过default swap、member swaps、non-member swaps、std::swap特化版本、以及对swap的调用,现在让我把整个形势做个总结。

        首先,如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。任何尝试置换(swap)那种对象的人都会取得缺省版本,而那将有良好的运作。

        其次,如果swap缺省实现版的效率不足(那几乎总是意味你的class或template使用了某种pimpl手法),试着做以下事情:

1. 提供一个public swap成员函数,让它高效地置换你的类型的两个对象值。稍后我将解释,这个函数绝不该抛出异常。

2. 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。

3. 如果你正编写一个class(而非class template),为你的class 特化std::swap。并令它调用你的swap成员函数。

        最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace,赤裸裸地调用swap。

        唯一还未明确的是我的劝告:成员版swap绝不可能抛出异常。那是因为swap的一个最好的应用是帮助classes(和class templates)提供强烈的异常安全性(exception-safety)保障。条款29对此主题提供了所有细节,但此技术基于一个假设:成员版的swap绝不抛出异常。这一约束只施行于成员版!因为swap缺省版本是以copy构造函数和copy assignment操作符为基础,而一般情况下两者都允许抛出异常。因此当你写下一个自定版本的swap,往往提供的不只是高效置换对象值的办法,而且不抛出异常。一般而言这两个swap特性是连在一起的,因为高效率的swaps几乎总是基于对内置类型的操作(例如pimpl手法的底层指针),而内置类型上的操作绝不会抛出异常。

 

请记住

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap。
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
  • 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内假如某些对std而言全新的东西。

 

个人心得:

        从该条款中可以看到,swap函数调用时,我们传递的对象进行交换时是采用深复制的手段,为了提高效率,改成用浅复制的方式,即交换指针对象。

        如果对象非常大,采用深复制就很慢,要重新申请内存,然后赋值等一系列操作,如果只复制指针对象就快得多,因为跟对象本身大小没什么关系,不过浅复制容易出现悬垂指针的问题。所以实际项目开发过程中需要视情况而定。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值