Effective C++ 学习笔记 条款25 考虑写出一个不抛异常的swap函数

swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程(exception-safe programming,见条款29)的脊柱,以及用来处理自我赋值可能性(见条款11)的一个常见机制。由于swap如此有用,适当的实现很重要。然而在非凡的重要性之外它也带来了非凡的复杂度。本条款探讨这些复杂度及因应之道。

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

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

只要类型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=的一般性实现细节,见条款10、11、12
        *pImpl = *(rhs.pImpl);
        // ...
    }
    // ...

private:
    WidgetImpl *pImpl;    // 指针,所指对象内含Widget数据
};

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

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

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

这个函数一开始的template<>表示它是std::swap的一个全特化(total template specialization)版本,函数名称之后的<Widget>表示这一特化版本系针对“T是Widget”而设计。换句话说当一般性的swap template施行于Widget身上便会启用这个版本。通常我们不能够(不被允许)改变std命名空间内的任何东西,但可以(被允许)为标准templates(如swap)制造特化版本,使它专属于我们自己的class(例如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);    // 若要置换Widget就置换其pImpl指针
    }
    // ...
};

namespace std
{
    template<> void swap<Widget>(Widget &a, Widget &b)    // 修订后的std::swap特化版本
    {
        a.swap(b);    // 若要置换Widget,调用其swap成员函数
    }
}

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

然而假设Widget和WidgetImpl都是class template而非class,也许我们可以试试将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 template偏特化,在function template身上偏特化是行不通的。这段代码不该通过编译(虽然有些编译器错误地接受了它)。

当你打算偏特化一个function template时,惯常做法是简单地为它添加一个重载版本,像这样:

namespace std
{
    // std::swap的一个重载版本(注意swap之后没有<...>),稍后会告诉你,这也不合法)
    template<typename T> void swap(Widget<T> &a, Widget<T> &b)
    {
        a.swap(b);
    }
}

一般而言,重载function template没有问题,但std是个特殊的命名空间,其管理规则也比较特殊。客户可以全特化std内的template,但不可以添加新的template(或class或function或其他任何东西)到std里头。std的内容完全由C++标准委员会决定,标准委员会禁止我们膨胀那些已经声明好的东西。所谓“禁止”可能会使你沮丧,其实跨越红线的程序几乎仍可以编译和执行,但它们的行为没有明确定义。如果你希望你的软件有可预期的行为,请不要添加任何新东西到std里头。

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

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

现在,任何地点的任何代码如果打算置换两个Widget对象,因而调用swap,C++的名称查找法则(name lookup rules,更具体地说是所谓argument-dependent lookup或Koenig lookup法则)会找到WidgetStuff内的Widget专属版本。那正是我们所要的。

上面所说的Koenig lookup法则具体如下,当遇到在对象上的方法调用时:
1.首先在对象本身的类中查找该方法。

2.如果在该类中找不到该方法,则会搜索与该类及其基类所在的命名空间。

这个做法对class和class template都行得通,所以似乎我们应该在任何时候都使用它。不幸的是有一个理由使你应该为class特化std::swap(很快会描述它),所以如果你想让你的“class专属版”swap在尽可能多的语境下被调用,你需得同时在该class所在命名空间内写一个non-member版本以及一个std::swap特化版本。

顺带一提,如果没有像上面那样额外使用某个命名空间,上述每件事情仍然适用(也就是说你还是需要一个non-member swap来调用member swap)。但,何必在global命名空间内塞满各式各样的class、template、function、enum、enumerant、typedef名称呢?难道你对所谓“得体与适度”失去判断力了吗?

目前为止所写的每一样东西都和swap编写者有关。换位思考,从客户观点看看事情也有必要。假设你正在写一个function template,其内需要置换两个对象值:

template<typename T> void doSomething(T &obj1, T &obj2)
{
    // ...
    swap(obj1, obj2);
    // ...
}

应该调用哪个swap?是std既有的那个一般化版本?还是某个可能存在的特化版本?抑或是一个可能存在的T专属版本而且可能栖身于某个命名空间(但当然不可以是std)内?你希望的应该是调用T专属版本,并在该版本不存在的情况下调用std内的一般化版本。下面是你希望发生的事:

template<typename T> void doSomething(T &obj1, T &obj2)
{
    using std::swap;    // 令std::swap在此函数内可用
    // ...
    swap(obj1, obj2);    // 为T型对象调用最佳swap版本
    // ...
}

一旦编译器看到对swap的调用它们便查找适当的swap并调用之。C++的名称查找法则确保将找到global作用域或T所在之命名空间内的任何T专属的swap。如果T是Widget并位于命名空间WidgetStuff内,编译器会使用“实参取决之查找规则”(argument-dependent lookup)找出WidgetStuff内的swap。如果没有T专属之swap存在,编译器就使用std内的swap,这得感谢using声明式让std::swap在函数内曝光。然而即便如此编译器还是比较喜欢std::swap的T专属特化版,而非一般化的那个template,所以如果你已针对T将std::swap特化,特化版会被编译器挑中。

因此,令适当的swap被调用是很容易的。需要小心的是,别为这一调用添加额外修饰符,因为那会影响C++挑选适当函数。假设你以这种方式调用swap:

std::swap(obj1, obj2);    // 这是错误的swap调用方式

这便强迫编译器只认std内的swap(包括其任何template特化),因而不再可能调用一个定义于它所处的较适当T专属版本。某些迷途程序员的确以此方式修饰swap调用式,而那正是“你的class对std::swap进行全特化”的重要原因,它使得类型专属之swap实现版本也可被这些“迷途代码”所用(这样的代码出现在某些标准程序库实现版中,如果你有兴趣不妨帮助这些代码尽可能高效运作)。

此刻,我们已经讨论过default swap、member swap、non-member swap、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(std命名空间中允许特化,而不允许重载swap)。并令它调用你的swap成员函数。

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

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

请记住:
1.当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。

2.如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非template,因为template特化std::swap时属于偏特化,而template function不允许偏特化),也请特化std::swap。

3.调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。

4.为“用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西(如swap的重载)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值