(Effective C++)第四章 设计与声明(Design and declaration)

6.1 条款18:让接口容易被正确使用,不易被误用(Make interfaces easy to use correctly and hard to use incorrectly)

条款13表明客户把新申请的资源置入智能指针中,以免资源泄露。而std::tr1::shared_ptr提供的某个构造函数接收两个实参,一个是被管理的指针,另一个是引用次数变成0时将被调用的“删除器“。想这样:
std::tr1::shared_ptr< Investment> createInvestment()
{
//建立一个NULL shared_ptr并以getRidOfInvestment为删除器
std::tr1::shared_ptr< Investment>   retVal(static_cast(Investment*)(0),
getRidOfInvestment);
     retval = …; //令retval指向正确对象
     return retval;
}
示例6-1-1  shared_ptr的一个构造函数
tr1::shared_ptr有个特别好的性质是:它会自动调用它的“每个指针专属的删除器“,因而消除另一个潜在的客户错误:所谓的”cross-DLL problem“。
好的接口很容易被正确使用,不容易被误用。促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容。

6.2 条款19:设计class犹如设计type(Treat class design as type design)

    略过。见《Effective C++》中文 第三版 P84.
    

6.3 条款20:宁以pass-by-reference-to-const 替换pass-by-value (Prefer pass-by-reference-to-const to pass-by-value)

这个规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较恰当。
缺省情况下,C++是以by value方式传递。
class Person {
public:
       Person();
       Virtual ~Person();
Private:
       std::string name;
std::string address;
};
class Student: public Person
{
public:
Student();
~Student();
Private:
       std::string schoolName;
std::string schoolAddress;
};
bool validateStudent(Student s);
Student plato;
bool platoIsOk= validateStudent(plato); //以by value传递方式调用函数
示例6-3-1  以by value方式传递参数
以plato为蓝本,对此函数的传递成本是“一次Student copy构造函数调用,加上一次Student析构函数调用“。而Student和Person有四个string对象,这样算起来有”六次构造和六次析构“。
更加高效的传递方式是pass by reference-to-const。
bool validateStudent(const Student &s);
这种传递方式没有任何构造函数或析构函数被调用。而const是必要的,因为不这样的话,调用者会忧虑validateStudent会不会改变他们传入的那个Student。
使用by reference 方式传递参数可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递被视为一个base class对象,只有base class的copy构造函数会被调用,切割了derived class对象的特性。
class Window {
public:
      std::string name() const;
      virtual void display() const;
};
class WindowWithScrollBars: public Window
{
public:
virtual void display() const;
};
void printNameAndDispaly(Window w)
{
  std::cout<< w.name();
w. display();
}
WindowWithScrollBars wwsb;
printNameAndDispaly(wwsb);  //以by value方式切割了特性
//正确的方式
void printNameAndDispaly(const Window & w)
{
  std::cout<< w.name();
w. display();
}
示例6-3-1  以by value方式传递参数

6.4 条款21:必须返回对象时,别妄想返回其reference(Don 't try to return a reference when you must return an object)

考虑到一个用以表现有理数(rational numbers)的class,内含一个函数用来计算两个有理数的成绩。
class Rational {
public:
       Rational (int numerator = 0, int denominator = 1)();//不声明为explicit why?
Private:
       int n,d;
friend const Rational operator*(const Rational &lhs, const Rational &rhs);
};
Rational a(1,2);  //a =1/2
Rational b(3,5);  //b=3/5
Rational c= a*b; //c=3/10
示例6-4-1  Rational类
期望“原本就存在一个其值为3/10的Rational对象“。
第一种方式:在stack空间创建。
const Rational operator*(const Rational &lhs, const Rational &rhs)
{
  Rational result(lhs.n*rhs.n, lhs.d*rhs.d);
  return result;
}
示例6-4-2  Rational类在stack空间创建
这个函数返回一个reference指向的result,但是result是个local对象,而local对象在函数退出前被销毁了。
第二种方式:在heap空间创建。
const Rational operator*(const Rational &lhs, const Rational &rhs)
{
  Rational *result= new Rational(lhs.n*rhs.n, lhs.d*rhs.d);
  return *result;
}
Rational w,x,y,z;
w = x*y*z; //new了两次,如何释放两次
示例6-4-3  Rational类在heap空间创建
同一个语句调用了两次operator*,因而调用了两次new,就需要两次delete。但是如何delete两次呢?
const Rational operator*(const Rational &lhs, const Rational &rhs)
{
  static Rational result;
result = (lhs.n*rhs.n, lhs.d*rhs.d);
  return result;
}
示例6-4-4  Rational类在stack空间创建static对象
上述函数不是多线程安全性函数。如果出现下列代码:
bool operator==(const Rational &lhs, const Rational &rhs)
Rational a,b,c,d;

if ((a*b)==(c*d))
{
}else{
}
不管怎么样,表达式(a*b)==(c*d)总是为真,不论a,b,c,d是什么值。因为调用端看到的永远是static Rational对象的现值。
正确的写法:
Rational operator*(const Rational &lhs, const Rational &rhs)
{
return Rational(lhs.n*rhs.n, lhs.d*rhs.d);
}
示例6-4-5  Rational类的operator*返回一个对象
当你必须在返回一个reference和返回一个对象之间抉择时,你的工作就是挑出行为正确的那个。

6.5 条款22:将成员变量声明为private(Declare data members private)

略过。见《Effective C++》中文 第三版 95.

6.6 条款23:宁以non-member、non-friend替换member函数 (Prefer non-member non-friend functions to member functions)

假设有这样的类,用来清除下周元素缓存区,清除访问过的URL的历史记录和移除系统中的所有cookies。
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
void clearEverything();  //成员函数调用前三个函数
};
void clearEverything(WebBrowser & wb)  //non-menber函数
{
wb. clearCache();
wb. clearHistory();
wb. removeCookies();
}
示例6-6-1  Rational类
我们推崇封装的原因:它使我们能够改变事物而只影响有限客户。能够访问private成员变量的只有class的member函数加上friend函数而已。如果一个member函数和一个non-member,non-friend函数之间做抉择,而且提供相同机能,那么,导致较大封装的是non-member,non-friend函数,因为它并不增加“能够访问class内的private成分“的函数数量。
注意事项
第一,这个论述只适用于non-member,non-friend函数。friend函数对class private成员的访问权利和member函数相同。
第二,只因在意封装性而让函数“成为class的non-member“,并不意味它”不可以是另一个class的member“。
在C++,比较自然的做法是让clearBrowser成为一个non-member函数并且位于WebBrowser所在的同一个namespace内:
namespace WebBrowserStuff{
class WebBrowser{…};
void clearBrowser(WebBrowser &wb);

}

6.7 条款24:若所有参数皆需类型转换,请为此采用non-member 函数(Declare non-member functions when type conversions should apply to all parameters)

假设你设计一个class类用来表现有理数,允许整数“隐式转换“为有理数似乎颇为合理。
class Rational {  //允许int-to-Rational隐式转换
public:
       Rational (int numerator = 0, int denominator = 1)();//不声明为explicit why?
       int numerator() const;
       int denominator() const;
const Rational operator*(const Rational &rhs) const; //条款3,20和21
};

Rational oneEight(1,8);  //a =1/8
Rational oneHalf(1,2);  //b=1/2
Rational result = oneEight * oneHalf; //很好
result = result * oneEight;         //很好
示例6-7-1  Rational类
然而当你尝试混合算术,只有一半行得通:
result = oneHalf * 2;    //很好
result = 2*oneHalf ;     //错误
乘法应该满足交换律,为什么?请看:
result = oneHalf.operator * (2);    //很好
result = 2.operator *(oneHalf) ;     //错误
是的,oneHalf是一个内含operator *函数的class的对象,所以编译器调用该函数。然而2整数并没有相应的class,也没有operator*成员函数。编译器也会尝试寻找可被如下这般调用的non-member operator*(也就是命名空间内或global作用域内):
result = operator *(2, oneHalf) ;     //错误
实际上并不存在这样一个接受int和Rational作为参数non-member operator *函数,所以查找失败。
还有,因为涉及non-explicit构造函数,编译器才会使result = oneHalf * 2合法,让2转换为Rational对象。如果Rational构造函数是explicit,以下语句没有一个编译通过。
result = oneHalf * 2;    //错误 (在non-explicit构造函数的情况下没问题)
result = 2*oneHalf ;     //错误 (甚至在non-explicit构造函数的情况下也是错误)
结论是,只有当参数被列为参数列内,这个参数才是隐式类型转换的合格参与者。

如果非要支持乘法交换律,就让operator*成为一个non-member函数,就允许编译器在每一个实参身上执行隐式类型转换。
class Rational {  //允许int-to-Rational隐式转换
public:
       Rational (int numerator = 0, int denominator = 1)();//不声明为explicit why?
       int numerator() const;
       int denominator() const;
};
const Rational operator*(const Rational &lhs,
const Rational &rhs); //现在成了一个non-member
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator ()*rhs.denominator());
}
Rational oneFourth(1,4);  //a =1/4
Rational result;
result = oneFourth * 2;         //很好
result = 2 * oneFourth;         //很好,很强大
示例6-7-2  Rational类的non-member
member函数的反面是non-member函数,不是friend函数。

6.8 条款25:考虑写出一个不抛异常的swap函数 (Consider support for non-throwing swap)

swap 就是将对象的值彼此赋予对方。std::swap典型实现:
namespace std{
template <typename T>
void swap(T&a, T&b){
T temp(a);
a = b;
b = temp;
}
}
示例6-8-1  std::swap的实现
只要T类型的支持copying行为,swap的功能就能完成。但是对于“以指针指向一个对象,内含真正的数据”这种类型,缺省的swap就没必要这么做。这种设计的常见表现形式是所谓“pimpl”手法(pimpl是“pointer to implementation”缩写)。
class WidgetImpl{ //针对Widget数据而设计的class
  public:

private:        //可能很多数据,意味复制时间很长
int a,b,c;
std::vector(double) v;

};
class Widget{ //针对Widget数据而设计的class
  public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) //复制Widget时,复制WidgetImpl对象
{

    *pImpl = *(rhs.pImpl);
    …
}

private:        
WidgetImpl *pImpl;  //指针指向对象所含数据
};
示例6-8-2  WidgetImpl的设计
一旦要置换两个Widget对象值,唯一做的是要置换其pImpl指针,但swap不知道,所以它不仅复制了三个Widgets,还复制了三个WidgetImpls对象。非常缺乏效率。
解决这个问题,要将std::swap针对Widget特化。
class Widget{ //增加swap函数
  public:
void swap(Widget & other)
{
    std::swap(pImpl, other.pImpl);
}

};
namespace std{
template <>  //修订后的std::swap特化版本
void swap<Widget>(T&a, T&b){
a.swap(b);
}
}
示例6-8-3  std::swap的特化版本
函数一开始“template <>”表示它是std::swap的一个全特化版本,函数之后的“<Widget>”表示这一特化版本针对“T是Widget”而设计的。我们可以将特化版本声明为friend,就可以访问Widget的成员变量,但是也可以在Widget类中声明一个public成员函数swap。

假设Widget和WidgetImpl都是class template而不是classes。如:
template<typename T>
Class WidgetImpl {…};
template<typename T>
class Widget {…};
在Widget内放个swap成员函数就像以往一样简单,但是却在std::swap时遇上乱流。
namespace std{
template <typename T>  //再次修订后的std::swap特化版本
void swap<Widget<T> >( Widget<T>&a,   //不合法
Widget<T>&b)
{
a.swap(b);
}
}
示例6-8-4  std::swap的不合法的特化版本
我们在企图特化(partially sepcialize)一个function template(std::swap),但是C++只允许对class template 偏特化,在function templates身上是行不通的。这段不该通过编译的。而惯例做法是,简单地为它加一个重载版本。
namespace std{
template <typename T>  // std::swap的重载版本
void swap (Widget<T>&a,  Widget<T>&b)
{
a.swap(b);
}
}
示例6-8-5  std::swap的重载版本
一般而言,重载function templates没有问题,但是std是个特殊的命名空间,不允许添加新的templates到std里面。正确的做法是声明一个non-member swap让它调用成员swap。
namespace WidgetStuff{
template<typename T>
Class WidgetImpl {…};
template<typename T>
class Widget {…};
template <typename T>  //非std空间的non-member版本
void swap (Widget<T>&a,  Widget<T>&b)
{
a.swap(b);
}
}
示例6-8-6  std::swap的重载版本
这个做法对classes和class templates都行得通。
一旦编译器看到对swap的调用,它们便查找合适的swap并调用之。C++名称查找法则,确保将找到global作用域或T所在的命名空间内的任何T专属swap。如果T是Widget并位于命名空间WidgetStuff内,编译器会根据实参取决之查找规则调用WidgetStuff的swap。如果没有T专属的swap存在,编译器会调用std的swap。
注意事项
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
如果你提供一个member swap,也应提供一个non-member来调用前者。对于classes(非templates),也请特化std::swap。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章