Effective C++条款25:设计与声明之(考虑写出一个不抛异常的swap函数)

一、标准模板库中的swap函数

  • 标准模板库中的swap函数,其是STL中的一部分,后来成为异常安全性编程(见条款29)以及用来处理自我赋值可能性(见条款11)的一个常见机制
  • 下面是STL中swap的源码:
    • 只要类型T支持拷贝(拷贝构造函数或者拷贝赋值运算符),缺省的swap函数就可能帮你对两个类型为T的对象进行兑换

二、设计自己的swap函数

  • 为什么不使用std::swap函数:
    • 从源码可以看出,std::swap函数,其涉及三个对象的复制操作:a复制到temp,b复制到a,temp复制到b
    • 但是对于我们自己设计的类来说,可能有时用不到这些复制操作,复制太多,效率太低

有些情况下不要使用std::swap函数

  • 例如我们有下面的一个类WidgetImpl,其中保存Widget的数据:
//针对Widget数据设计的class
class WidgetImpl
{
public:
    //...
private:
    int a, b, c;
    std::vector<double> v;
    //...其他数据
};
  • 此时又有一个类Widget,其采用pimpl手法(参阅条款31),里面保存一个指针指向于一个对象(此处为WidgetImpl),该对象内含真正的数据:
//这个class使用pimpl手法
class Widget
{
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs)//拷贝赋值运算符
    {
        //...
        *pImpl = *(rhs.pImpl);
        //...
    }
private:
    WidgetImpl* pImpl;
};
  • 我们真正使用时使用的的是Widget对象,如果我们使用std::swap函数,那么其会进行3次Widget对象的复制,并且每次复制的时候调用的是上面Widget类中的operator=运算符。因此std::swap函数会调用三次operator=函数,这种效率是非常低的

使用全特化的swap函数

  • 我们希望让std::swap知道,当Widget类进行替换的时候只需要替换其内部的pImpl指针就可以了,此时我们可以针对std::swap做一个全特化的版本。如下:
//template<>用于表示这是一个std::Swap的全特化版本
//此swap用于表示这个函数针对“T是Widget”而设计的
namespace std {
    template<>
    void swap<Widget>(Widget& a, Widget&b) 
    {
        swap(a.pImpl, b.pImpl);
    }
}
  • 但是上面的代码是无法编译通过的,因为pImpl是private的,因此函数无法编译通过

为类设计一个swap成员函数,并设计一个全局swap函数

  • 由于pImpl是private的,因此上面的全特化的swap函数无法编译通过。此时我们可以修改Widget class,而使全特化的版本可以应用于我们的Widgte class:
class Widget
{
public:
    void swap(Widget& rhs)
    {
        using std::swap;
        swap(pImpl, rhs.pImpl);//调用std::swap函数
    }
private:
    WidgetImpl* pImpl;
};

namespace std {
    template<>
    void swap<Widget>(Widget& a, Widget&b) 
    {
        a.swap(b);//调用Widget::swap函数
    }
}
  • 现在的代码可以编译通过了,并且可以使用我们全特化的swap版本了

三、偏特化遇到的问题

  • 如果Widget和WidgetImpl都是模板类,而不是普通的类,如下所示:
template<typename T>
class WidgetImpl { };

template<typename T>
class Widget { };
  • 此时如果我们我们还想偏特化一个针对于Widget的swap函数,那么就会出出错,因为:
    • 我们企图偏特化一个function template(std::swap),但C++只允许对class template偏特化,因此这里是错误的
namespace std{
    template<typename T>
    void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}
  • 因此如果我们想偏特化一个function template,那么就需要为其添加一个重载版本的swap函数
namespace std{
    //这里std::swap的一个重载版本
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}

四、放弃重载版本的swap函数

  • 上面我们重载了std::swap函数,但是std是个特殊的命名空间,其管理规则比较特殊,客户可以全特化std内的template,但是不可以添加新的template到std里面
  • 因此我们我们应该将偏特化版本的swap函数置于我们自己的命名空间中
namespace WidgetStuff{ //自己命名的命名空间
    //...
    template<typename T>
    class Widget { };	
    //...

    template<typename T>
    void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}
  • 现在我们在任何地方调用swap()函数转换两个Widget对象,那么上面的swap函数就会被调用

五、使用using声明

  • 上面的做法对于class或class template都行的通,但不幸的是你应该为class特化std::swap(下面会介绍)
  • 如果你想让你的“class专属版”swap在尽可能多的环境下被调用,你需要同时在该calss所在命名空间内写一个non-member版本以及一个std::swap全特化版本
  • 另外,其实我们也可以不使用上面的命名空间,这些规则也可以生效
  • 如果我们正在写一个function template,其内需要调用两个对象值:
template<typename T>
void doSomething(T& obj1,T& obj2)
{
    //...
    swap(obj1,obj2);
    //...
}
  • 上面的swap应该调用哪一个版本呢?是可能存在的特化版?还是存在T专属版本而且还可栖息于某个命名空间内?你希望的应该是调用T专属八本,并且该版本不存在的情况下调用std内的一般化版本。此时修改代码如下:
template<typename T>
void doSomething(T& obj1,T& obj2)
{
    using std::swap;  //使std::swap在次函数内可用
    //...
    swap(obj1,obj2);
    //...
}
  • 一旦编译器看到对swap函数的调用,它会查找适当的swap函数并调用
  • 需要注意,下面添加额外的修饰符是错误的,因为强迫编译器调用swap函数,而永远都不会调用专属于T的版本了
template<typename T>
void doSomething(T& obj1,T& obj2)
{
    //...
    std::swap(obj1,obj2); //错误的swap调用方式
    //...
}

六、swap成员函数不能抛出异常

  • 成员本的swap函数绝不可能抛出异常。因为swap的一个最好的应用是帮助类(和类模板)提供强烈的异常安全性保障(条款29介绍)
  • 但此技术基于一个假设:成员版的swap绝不抛出异常。这一约束只施加于swap成员版本,不可施加于非成员版本,因为swap缺省版本是以拷贝构造函数和拷贝赋值运算符为基础,而一般情况下两者都允许抛出异常
  • 因此当你写下一个自定义的swap,往往提供的不只是高效置换对象的版本,而且还不抛出异常

七、总结

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非template class),也请特化std::swap
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”
  • 用“用户定义类型”进行std template全特化时最好的,但千万不要尝试在std内加入某些对std而言全新的东西
发布了1462 篇原创文章 · 获赞 996 · 访问量 35万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104311968