《Effective C++》学习笔记——条款25

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

四、Designs and Declarations

Rule 25:Consider support for a non-throwing swap

规则 25:考虑写出一个不抛异常的 swap 函数

swap 是一个有趣的函数。

原本它只是STL的一部分,而后成为异常安全性编程(exception-safe programming,详见条款29)的脊柱,以及用来处理自我赋值可能性的一个常见机制。

然而在非凡的重要性之外它也带来了非凡的复杂度。

本条款将探讨这些复杂度及因应之道。

1.一些基本的东西

所谓swap(置换)两对象的值,意思是将两对象的值彼此赋予对方。

缺省的情况下 swap动作可由标准程序库提供的swap算法完成。

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

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

但是对于某些类型而言,这些复制动作无一必要,对它们而言swap的缺省行为太慢了!

2.以指针指向一个对象,内含真正数据

上面的某些类型中,最主要的就是这种——以指针指向一个对象,内含真正数据。

对于这种设计的常见表现形式是所谓的 “pimpl 手法”(pimpl是 "pointer to implementation"的缩写,详见条款31)。

用这种手法设计Widget class,就是这样:

class WidgetImpl  {    // 针对Widget数据而设计的class
public:
    ...
private:
    int a,b,c;
    std::vectorK<double> v;    // 意味着复制时间更长
    ...
};

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

一旦要置换两个Widget对象值,我们唯一需要做的就是置换其pImpl指针,但缺省的 swap算法不知道这一点。

它不只复制三个Widgets,还复制了三个WidgetImpl对象。

非常非常非常缺乏效率!

我们希望能告诉 std::swap:当Widgets被置换时真正该做的是置换其内部的pImpl指针。

确切实践这个思路的一个做法是:将std::swap针对Widget特化。

下面是基本构想:

namespace std  {
    template<>
    void swap<Widget>( Widget& a, Widget& b )
    {
        swap(a.pImpl,b.pImpl);    // 只要置换它们指针即可
    }
}

但是,很遗憾这种形式,无法通过编译。

这个函数,一开始的"template<>"表示它是 std::swap的一个全特化版本,

函数名称之后的 <Widget>表示这一特化版本是针对“T是Widget”而设计。

换句话说:当一般性的swap template施行于 Widgets身上便会启用这个版本。

通常我们不能够(不被允许)改变std命名空间内的任何东西,但可以(被允许)为标准templates(如swap)制造特化版本,使它专属于我们自己的classes(例如Widget)。

> 但是!

这个函数无法通过编译,因为它企图访问a和b内的pImpl指针,但那个是 private!

解决方法:

?1、我们可以将这个特化版本声明为friend

?2、我们可以令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 )
    {
        a.swap(b);
    }
}

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

3.如果面对class templates而非classes

然而假设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);  }
}

这个看起来合情合理,但是不合法。

因为我们企图偏特化一个function template(std::swap),但C++只允许对class templates进行偏特化,在function templates身上偏特化是行不通的。

这段代码不该通过编译(虽然有些编译器错误地接受了它)。

当你打算偏特化一个function templates时,惯常的做法是简单地为它添加一个重载模板:

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

一般而言,重载function templates没有问题,但std是个特殊的命名空间,其管理规则也比较特殊。

客户可以全特化std内的templates,但不可以添加新的templates(或classes或functions或其他任何东西)到std里头。

std的内容完全由C++标准委员会决定,标准委员会禁止我们膨胀那些已经声明好的东西。

其实,违反上面那些,也可以通过编译,但是会产生不可预期的行为。

所以,断了那个念头吧。

4.non-member函数,再次出场

声明一个non-member swap让它调用 member swap,

但不再将那个non-member swap声明为std::swap的特化版本或重载版本。

假设Widget的所有相关机能都被置于命名空间WidgetStuff内,就是这样:

namespace WidgetStuff  {
    ...
    template<typename T>
    class Widget  {  ...  };
    ...
    template<typename T>
    void swap( Widget<T>& a,Widget<T>& b )    // 这里并不属于std命名空间
    {
        a.swap(b);
    }
}

现在,任何地点任何代码如果打算置换两个Widget对象,因而调用swap,C++的名称查找法则会找到WidgetStuff内的Widget专属版本。

>PS:

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

可惜,有一个理由让你应该为classes特化std::swap,稍后将讲述。

所以,如果你想让你的“class 专属版”swap在尽可能多的语境下被调用,你需同时在该class所在命名空间内写一个non-member版本以及一个std::swap特化版本。

目前为止,我们所写的每一样东西都和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并调用之。

>额外knowledge:

? C++的名称查找法则(name lookup rules )确保将找到global作用域或T所在之命名空间内的任何T专属的swap。

? 如果T是Widget并位于命名空间WidgetStuff内,编译器会使用“实参取决之查找规则”找出WidgetStuff内的swap。

? 如果没有T专属之swap存在,编译器就会使用std内的swap,这需要感谢using声明式让std::swap在函数内曝光。

? 然而即便如此编译器还是比较喜欢std::swap的T专属特化版,而非一般化的那个template,所以如果你已针对T将std::swap特化,特化版本会被编译器挑中。

注意,

令适当的swap被调用是很容易的。

但不要为这一调用添加额外的修饰符,因为那会影响C++挑选适当函数。

比如用这种方式,就是错误的:

std::swap(obj1,obj2);

5.小小总结一下吧

我们已经讨论过default swap、member swaps、non-member swaps、std::swap特化版本 以及 对swap的调用。

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

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

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

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

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

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

(4) 唯一还未明确的就是:成员版swap决不可抛出异常。

6.请记住

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

☆ 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于 classes(而非templates),也请特化 std::swap。

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

☆ 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

时间: 2024-10-12 04:48:20

《Effective C++》学习笔记——条款25的相关文章

effective c++学习笔记条款23-25

条款23:宁可用非成员,非友元函数来替代成员函数 1.非成员函数提供了更好的封装性,这个函数内不能访问类的私有成员,封装的越严密我们对类的数据就可以弹性越大的操纵,因为可见这些数据的客户越少,反之数据影响的客户也就越少. 2.c++比较自然的做法-(关系到标准库numplace的组织结构),可以把不同便捷函数放到不同Namespace去,让客户来决定要用的非成员函数功能,这是类不能提供的. 条款24:若所有参数皆需类型转换,请为此采用非成员函数. 1.如果你需要为某个函数的所有参数(包括被thi

effective c++学习笔记条款11-13

条款11: 1.令赋值运算符返回一个&,因为STL,string都是这样做的,除非你有足够好的理由不这样做. 2.处理自我赋值的方法----(1).在没有成功获取对象数据时不要删除自己的数据,避免发生异常后原对象指针是一个悬浮指针 (2).判断自我赋值的检查操作会耗费不少时间,可以用swap交换数据技术来优化---(1)形参为赋值而来,(2)形参为静态引用,多加一个函数内拷贝操作.

effective c++学习笔记条款8-10

条款7:为多态基类声明虚析构函数 1.一个基类指针接受一个派生类对象的地址时,对该指针delete,仅仅释放基类部分 2.给所有类都带上虚析构函数是个馊主意,会带有vptr指向一个函数指针数组,扩大不必要的对象大小,除非补偿vptr,否则没有移植性. 3.string类和STL不含有虚析构函数,然而一些用户 却将他们作为基类,运用   delete指向派生类的基类指针,导致错误[c++11添加了禁止派生性质],他们不适合当基类. 4,手头上没有合适的纯虚函数,但你确实需要一个抽象类,把析构函数声

effective c++学习笔记条款20-22

条款20:用引用传递代替值传递 1.尽量以引用传递来代替传值传递,前者比较高效,并且可以避免切割问题 2.以上规则不适用于内置类型,以及STL的迭代器,和函数对象 条款21:必须返回对象时,别妄想返回对象的引用 1.绝对不要返回指针和引用指向一个局部对象或者静态局部对象而有可能需要多个这样的对象,条款4已经为在单线程环境合理返回&指向一个局部静态提供了一份设计实例.(保护初始化顺序) 条款22:将成员变量声明为private 1.切记将成员变量声明为private.这可赋予客户访问数据的一致性,

effective c++学习笔记条款4-7

条款4:确定对象被使用前已经初始化 一. 变量在不同情况下可能会初始化,也可能不会初始化. 注意初始化和赋值的区别. 1.在类中内置类型不会发生隐式初始化,自定义有默认构造函数的能被默认初始化 所以在构造类时务必初始化内置类型,最好给自定义的对象显示初始化避免在函数体中赋值浪费资源. 2.内置类型在函数体内不会初始化,在函数体外自动初始化为0. 二. 1.const和引用类型必须初始化,不可能赋值 三 1.当类实在是有较多构造函数,并且总是要对一些成员数据重复初始化,可以考虑将那些“赋值和初始化

effective c++学习笔记条款17-19

条款17:以独立语句将New对象放置入智能指针. 1.以独立语句将newed对象放置入智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露. void name(shared_ptr<管理对象类型>(new 管理对象类型),其它函数)),New被分配内存不一定马上放入管理对象,因为有其它函数干扰,这不是独立语句. 条款18:让接口容易被正确使用,不易被误用. 1.好的接口很容易被正确使用,不容易被误用.你应该在你的所有接口中努力达成这些性质. 2.“促进正确使用”的办法包括接

effective c++学习笔记条款35-37

#include<iostream> using namespace std; class A { public: void asd() { pri(); } private: /*virtual*/ void pri() { cout << "基类函数" << endl; } }; class B :public A { private: void pri() /*override*/ { cout << "派生类函数&quo

effective c++学习笔记条款29-31

条款29:为异常安全而努力是值得的[回顾] 1.异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏,这样的函数分为3种可能的保证:基本型,强烈型,不抛异常型 2.“强烈保证”往往能通过copying and swap 来实现出来,但并非所有函数都可实现或者具备现实意义. 3.函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全中”的最弱者.

effective c++学习笔记条款26-29

条款26:尽可能延后变量定义式的时间 1.中途抛出异常浪费构造函数 2.在循环内定义变量,消耗n个构造函数,n个析构函数:在循环外定义变量消耗n个赋值函数,1个构造,一个析构: 除非赋值的消耗比构造和析构少的不少,或者你处理的代码效率高度敏感,还是在循环内定义变量吧. 条款27:尽量少做转型动作 1.const_cast-----脱离常量属性,static_cast(隐式转换显示化),dynamic_cast(从一个寄放派生类的基类指针或引用调用派生类的成分),reinterpret_cats低