C/C++编程:考虑写出一个不抛异常的swap函数

1059 篇文章 286 订阅

swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来实现自我赋值可能性的一个常见机制。

原理

所谓swap两对象值,就是将两对象的值交换。缺省情况下的swap动作可有标准库提供的swap算法完成。典型实现如下:

namespace std{
	template<typename T>
	void swap(T & a, T & b){
		T temp(a);
		a = b;
		b = temp;
	}
};

只要类型T支持拷贝(通过拷贝构造函数和拷贝运算符),缺省的swap就可以完成交换两相同类型的对象的值,你不需要为此做任何动作。

上面版本的实现调用了三次拷贝,对于某些类型而言,太慢了。典型的就是"以指针指向一个对象,内含指针数据"的那种类型。这种设计的常见表现形式就是"pimpl(pointer to implementation)手法"。如果以这种手法设计Widget类,看起来会像这样:

class WidgetImpl{
private:
	int a, b, c;
	std::vector<double> v; // 可能有很多数据,意味着复制时间很长
};

class Widget{  // 这个类使用pimpl手法
public:
	Widget(const WidgetImpl& rhs);
	Widget& operator=(const Widget& rhs){ //复制Widget时,令它复制其WidgetImpl对象
		...
		*pImpl = *(rhs.pImpl);
	}

private:
	WidgetImpl * pImpl;
}

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

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

namespace std{
	template<>
	void swap<Widget>(Widget&a, Widget&b){
		swap(a.pImpl, b.pImpl);  // 只需要交换它们的pImpl指针就好
	};
};

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

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


class Widget{ 
public:
	...
	void swap(Widget& other){
		using std::swap;     
		swap(pImpl, other.pImpl);
	}
}

namespace std{
	template<>
	void swap<Widget>(Widget &a, Widget & b){
		a.swap(b);
	}
};

这种做法不止能够通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swap特化版本.

假如Widget、WidgetImpl但是类模板而不是类,这时候我们可以尝试将WidgetImpl内的数据类型加以参数化:

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

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

参考上面我们应该这样写:

namespace std{
    template<typename T>
    void swap<Widget<T>>(Widget<T> &a, Widget<T> & b) {
        a.swap(b);
    }
};

但是无法通过编译:因为C++只允许对类模板偏特化,不允许在函数模板身上偏特化。如果你打算偏特化一个函数模板,常用做法是简单的为它添加一个重载版本:

namespace std{
    template<typename T>
    void swap(Widget<T> &a, Widget<T> & b){
        a.swap(b);
    }
};

一般来说,重载函数模板没有问题,但是std是个特殊的命名空间,其管理规则也特殊:客户可以全特化std内的模板,但是不可以添加新的模板到std里面。std的内容完全由C++标准委员会决定。虽然上面可以通过编译,但是行为未定义。

那怎么办?解决方法:声明一个non-member swap让它调用member swap,但不再将那个non-member swap声明为std的特化版本

namespace WidgetStuff{
	... // 模板化的WidgetImpl等等
	template<typename T>
	class Widget{...};  //同前,内含swap成员函数
	...
	template<typename T>  //non-member  swap函数
	void swap(Widget<T>&a, Widget<T>&b){
			a.swap(b);
	}
};

总结

首先,如果swap的缺省实现码对你的类或者类模板提供可接受的效率,你不需要做任何事情。任何尝试交换对象的人将使用缺省版本达到目的

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

  • 提供一个public swap成员函数,让他搞笑的置换你的类型的两个对象值。这个函数就不该抛出异常
  • 在你的类或者模板所在的命名空间内提供一个non-member swap,并令它调用上面的swap成员函数
  • 如果你正在编写一个类(而不是类模板),为你的类特化std::swap。并令它调用你的swap成员函数。
  • 最后,如果你调用swap,请确定包含一个using声明式,以便让你的std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸的调用swap

请记住

  • 当你的std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常(为这个类提供强烈的异常安全性)
  • 如果你提供一个成员swap,也该提供一个non-member swap来调用前者。对于类(注意不是模板),也请特化std::swap
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何"命名空间资格修饰"
  • 为"用户自定义类型"进行std模板全特化是好的,但是千万不要在std内加入某些对std而言全新的东西
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值