EffectiveC++条款25的理解

Effective C++条款25的理解

这个条款的全称是考虑写出一个不抛出异常的swap函数,通过研究swap函数的各种改善方法来教你设计一个高效的函数。在阅读过程中,对于全特化和偏特化,类和类模板和函数模板这几者之间的关系不太理解,这里写下自己的一些思考。

一、为什么要进行特化?

进行特化的原因是为了更高的效率,对于特定的类型,如果你有更好的实现方案,那么编译器就会调用特化版本而不是默认缺省版本。特别典型的应用便是“pimpl”手法,pimpl全称是“pointer to implementation”,也就是“以指针指向一个对象,内含真正类型”。书中代码如下:

class WidgetImpl{
        public:
        ...
        private:
        int                      a,b,c;
        std:vector<double>       v;
        ...
};
class Widget{
        public:
            Widget(const Widget& rhs);//拷贝构造函数
            Widget& operator=(const Widget& rhs){
                   ...
                   *pImpl=*(rhs.pImpl) ;
                   ...
            }
        private:
            WidgetImpl* pImpl;
};

如果要置换两个Widget对象值,则利用拷贝构造函数和赋值操作符,临时构造Widget对象temp作为中转站实现swap,根据重载后的成员函数operator=的函数体,*pImpl=(rhs.pImpl),说明不止进行了3次Widget复制,还进行了3次WidgetImpl的复制,而WidgetImpl里含有大量数据,复制起来十分耗时。
更为高效的做法是置换其内部的pImpl指针,实践的思路是将std::swap针对Widget进行特化。

二、针对class的解决方案

如同书中所描述的方案一样,我们令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);
    }
}

此处值得注意的有几点:
①swap本身是函数模板,function template只能全特化不能偏特化,因为已经有了函数重载,偏特化没有必要。
②客户可以全特化std内的template,但是不建议添加新的template(包括class或function或者其他任何东西)到std当中。
③Widget类内成员函数swap使用了using std::swap,使得std版本的swap在函数内曝光,得以使用。

三、针对class template的情况

如果Widget和WidgetImpl是class template而不是class,那么和class的情况有什么不同?class的情况就像上文代码:

template<>
void swap<Widget>(Widget& a,Widget& b)

“template<>”表示它是一个全特化的版本,函数名称之后的Widget表示这一特化版本是针对“T是Widget”而设计。
当Widget是class template,首先需要将Widget template实例化一个class:

template<typename T>
Widget<T>

之后再带入swap中使用,综合起来代码如下:

template<typename T>
void swap<Widget<T>>(Widget<T>& a,Widget<T>& b)

但就像第二部分的注意点①,这样是对swap的偏特化,偏特化并不仅仅是指特殊部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本,这里对模板参数T更进一步地进行了Widget的限制,所以是模板函数偏特化了。为什么模板函数不能偏特化,可以参考这篇文章参考链接
当你打算偏特化一个函数模板时,惯常做法是为它添加一个重载版本:

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

然后问题又来了,根据第二部分注意点②,客户只能全特化已有的swap函数模板,不能添加新的template到std中,而往std中加swap的重载版本就等于是添加新的函数模板了,std的内容完全由C++标准委员会决定,委员会禁止我们膨胀那些已经声明好的东西。那么这个问题怎么解决呢?
答案也很简单:我们还是声明一个non-member swap函数,令其调用member swap,但是non-member sawp不再是std空间内的函数模板,而是放在一个新的命名空间中,我们可以把Widget的相关机能全部放在一个新的命名空间WidgetStuff中,代码如下:

namespace WidgetStuff {
    ... 
    template<typename T>
    class Widget {...};//内含真正执行置换的member swap 
    template<typename T>
    void swap(Wiget<T>&a, Widget<T>&b) 
    {
    a.swap(b);
    }
}

四、class和class template的统一

这种设置一个新的namespace(WidgetStuff)并在其中进行相关机能的做法对于class和class template都行的通,我们可以将class和class template的做法统一起来。对于class来说我们似乎并不需要像第一部分所说的那样在std空间内全特化一个std::swap,因为我们在新的namespace中已经做过了,但是有些人会为swap的调用添加额外的修饰符:

std::swap(obj1,obj2);

这就使得编译器只认std内的swap,WidgetStuff内的高效率swap不会被调用,如果不全特化std::swap的话,又会回到没有效率的默认swap函数。因此在global命名空间std内对于Widget全特化一个swap函数模板是必要的,它能使Widget专属的swap实现版本不至于因为不恰当的前缀而无法使用。
并且这种做法也和STL容器具有一致性,因为所有STL容器也都提供有public 成员函数和std::swap的特化版本(用以调用前者)。
因此不管怎么说,当针对的是class,你都需要全特化std::swap。

五、using std::swap的必要性

分析代码,我们发现在调用swap函数之前,都需要包含一个using声明式,使得std内的swap曝光,为什么需要这个using声明式呢?
一个原因是编译器使用ADL(arguement-dependent lookup)规则,这是一个名字查找规则,它的规则就是当编译器对无限定域的函数调用进行名字查找时,除了当前名字空间域以外,也会把函数参数类型所处的名字空间加入查找的范围。比如:

template<typename T>
void dosomething(T& obj1,T& obj2)
{
    using std::swap;
    ... 
    swap(obj1,obj2);
    ...
}

此处的swap应该调用哪个版本的swap呢?如果obj1和obj2的类型参数是Widget并且位于命名空间WidgetStuff内,编译器根据ADL规则会把WigetStuff加入查找范围,那么就能找到WidgetStuff内的swap,如果在这个命名空间中没有Widget专属的swap,那么就需要在std范围内查找swap了,这就需要using将std::swap曝光。编译器挑选的顺序如下:
* 1.WidgetStuff内的专属Wiget特化版swap
* 2.std内的Widget特化版swap
* 3.一般化的swap template
我们在第一部分的代码中也使用了using std::swap,因为class Widget可能在WidgetStuff命名空间中,而WidgetStuff中并没有swap(指针,指针)这个函数,就需要引入std命名空间的swap了。
第五部分的理解可以查看StackOverflow上的这个回答:参考链接

六、总结

书上总结的很清楚了,这边再啰嗦一下。
首先,当std内提供的swap缺省实现码对你的class和class template的效率影响可以接受,就不要做任何事情。
如果swap的缺省实现版效率不足,比如使用了某种pimpl手法,就尝试做以下的事情:
* 1.在class或template内写一个public swap成员函数,能高效地置换你的类型的两个对象值。
* 2.在你的class或template所在的命名空间内提供一个non-member swap,令它调用上述swap成员函数。
* 3.如果你正在编写一个class(而不是 class template),为你的class特化std::swap,并令它调用上述swap成员函数。
* 4.如果调用swap函数,请确定包含一个using std::swap;使得std::swap函数在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值