zoukankan      html  css  js  c++  java
  • EffectiveC++ 第4章 设计与声明

    我根据自己的理解,对原文的精华部分进行了提炼,并在一些难以理解的地方加上了自己的“可能比较准确”的「翻译」。

    Chapter4 设计与声明 Designs and Declarations

    条款18: 让接口容易被正确使用,不易被误用

    欲开发一个“容易被使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。

    假设我们要设计一个表示日期的class:

    class Data{
    public:
        Date(int month,int day,int year);
        ...
    };
    

    事实上使用它的客户很容易犯错误:

    以错误的次序传参:

    Date d(30,3,1995); //喔哟! 应该是“3,30”而不是"30,3"

    传递无效的参数:

    Date d(2,30,1995); //2月有30号????

    我们可以引入类型系统(type system)和外覆类型(wrapper types)
    现以外覆类型来区别天数,月份,年份,然后再Date中使用:

    struct Day{                      struct Month{
        explicit Day(int d)              explicit Month(int m)
            :val(d) {}                       :val(m) {}
        int val;                         int val;
    };                               };
    
    struct Year{
        explicit Year(int y)
            :val(y) {}
        int val;
    };
    
    class Date{
    public:
        Date(const Month& m,const Day& d,const Year& y);
    	  ...
    };
    Date d(30,3,1995); //not ok
    Date d(Day(30),Month(3),Year(1995)); //not ok
    Date d(Month(3),Day(30),Year(1995)); //ok
    

    其实你也可以用更成熟的class来封装外覆类型,但这里的struct已经很好了。

    类型确定后,通常要对值进行限制,比如一年只有12个月。你可以用enum来表现月份。但是enum不具备类型安全性,比如enums可以被拿来当一个ints使用。

    比较安全的解法是预先定义所有有效的Months:

    class Month{
    public:
        static Month Jan() { return Month(1); }
        static Month Feb() { return Month(2); }
        ...
        static Month Dec() { return Month(12); }
        ...
    private:
        explicit Month(int m);
        ...
    };
    Date d(Month::Mar(),Day(30),Year(1995));
    

    常见的预防客户出错的办法是限制类型内的权限。例如加上const。

    另外,::应尽量让你的包装types与内置types行为一致::。客户已知道int这样的type会有什么行为,所以你应让你包装的types也有相同表现。<避免无端与内置类型不兼容>

    记住,任何接口如果试图让客户「必须记得做某些事情」,就是有着「不正确使用」的倾向。

    还记得条款13吗,tr1::shared_ptr接受了func()返回的指针,这将发挥智能指针的威力。
    但是如果客户自己就忘记了要用到智能指针呢?较佳接口的设计原则是先发制人,也就是这样写func():

    std::tr1::shared_ptr<xx> func();

    这实质上强迫客户将返回值储存于一个tr1::shared_ptr内,让接口设计者得以阻止一大群客户犯下资源泄漏的错误。

    还有一种特殊情况。假设作为class设计者的你想让那些“从func()取得xx*指针”的客户将该指针传递给一个名为getRidOfxx()的函数,并让它处理这个指针的「销毁」,而不是粗暴地直接对此指针使用delete。可能你这样设计有出于你的考虑,但是客户还是可能忘记并仍使用delete。所以func()的设计者可先发制人,不仅返回一个tr1::shared_ptr,并在它身上绑定删除器(deleter) getRidOfxx()。

    事实上tr1::shared_ptr有一个重载的构造函数接受两实参:一个是被管理的指针,另一个是当引用次数变为0时被调用的“删除器”:

    std::tr1::shared_ptr<xx> pInv(0,getRidOfxx);
    //企图创建一个null智能指针,但是无法通过编译。
    

    这个构造函数坚持第一个参数必须是指针,而不是int型的值0———虽然它能被转换为指针。所以转型可解决:

    std::tr1::shared_ptr<xx> pInv(static_cast<xx*>(0),
        getRidOfxx);
    
    //static_cast以后提到.
    

    而作为func()的内部实现:

    std::tr1::shared_ptr<xx> func()
    {
        std::tr1::shared_ptr<xx> retVal(static_cast<xx*>(0),
    											getRidOfxx);
    	  retVal = ...; //令retVal指向正确对象
        return retVal;
    }
    

    当然,若将被pInv接管的原始指针已经在建立pInv之前确定了,那么直接传此指针给pInv构造函数是更佳选择。


    条款19: 设计class犹如设计type

    实际上,你定义一个新class时,可理解为你定一个了新type。

    这意味着你不仅是class设计者,还是type设计者,重载(overloading)函数和操作符、控制内存的分配和归还、定义对象的初始化和终结……都在你手上

    考虑以下问题,你的回答往往影响你的设计规范:

    • 新class,或者说type该如何被创建和销毁?

    这将影响你的构造、析构、内存分配与释放函数:
    (operator new, operator new[], operator delete, operator delete[])

    前提是你打算撰写它们

    • 对象初始化和赋值该有怎样的差别?

    决定了你的构造函数和赋值操作符的行为。别混淆“初始化”和“赋值”,它们对应不同的函数调用(条款4)。

    • 新type对象若被passed by value(以值传递),意味着什么?

    考虑copy构造函数用来定义一个type的pass-by-value该如何实现

    • 什么是新type的“合法值”?

    对于你设计的class成员变量,你必须考虑它们取值的范围以及规范(约束条件),这决定了你的成员函数必须进行的错误检查工作。它也影响函数抛出的异常。

    • 你的type需要配合某个继承图系吗?

    如果你的type继承自现有的classes,就会受到设计约束。特别是受到“它们的函数是virtual或non-virtual”的影响。若你允许其它classes继承你的class,这要考虑你的函数是否为virtual。

    • 你的type需要什么样的转换?

    如果你希望你的type T1能隐式转换为T2,就必须在class T1内写一个类型转换函数( operator T2 )或在class T2内写一个non-explicit-one-argument(可被单一实参调用)的构造函数。若你只允许显式转换,就得写出专门负责执行转换的函数。

    • 什么样的标准函数应驳回? 那些是你应声明为private的成员(条款6)

    • 谁该取用新type的成员?

    这将帮助你决定哪个成员为public、private、proteced。也帮你决定哪个class,functions应该是友元,以及它们的嵌套是否合理。


    条款20: 宁以pass-by-reference-to-const替换pass-by-value

    C++默认是以by value方式(继承自C)传递对象至函数(或来自函数)。这样一来,函数参数都是以实参的副本为初值,而调用端获得的亦是函数返回值的副本。这些副本是由对象的copy构造函数产出,会成为费时的操作。

    考虑以下代码:

    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); //by value
    Student plato;
    bool platoIsOk = validateStudent(plato);
    

    无疑,会以plato为蓝本初始化s,返回后s被销毁。
    你会发现,在这里,以by value传递一个Student对象会导致调用一次Student copy构造函数、一次Person copy构造函数、四次string copy构造函数。

    但如果以pass by reference-to-const的方式,效率会高得多:

    bool validateStudent(const Student& s);

    这时,没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。这个const修饰符很重要,它可以保证函数不会修改源头的Student。

    另外,by reference方式可避免slicing(对象切割)。假设base class为A,derived class为B,有这种代码:

    void func(A obj)..

    而你传参的操作:

    B tmp = new B();
    func(tmp);
    

    此时A的copy构造函数被调用,但是属于B的特质化成员会被无视掉,只剩A对象的框架。此时解决方案即为以by reference-to-const方式传递参数。

    当应用于C++内置类型,如int之类,pass-by-value可能会更高效,这同样适用于STL迭代器和函数对象。


    条款21: 必须返回对象时,别妄想返回其reference

    在尝到传引用的甜头后,你可能从此一发不可收拾。但是你总有一次会犯下致命错误:开始传递一些references指向实际不存在的对象。

    现在假设有一个表达有理数(Rational Number)的Class:

    class Rational{
    public:
        Rational(int numerator = 0,
                 int denominator = 1); //分别表示分子和分母
        ...
    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

    第三条语句相当于 Rational c = operator*(a,b); ,这时函数会返回适当的「值」赋给c。

    现在看第一个版本的*运算重载函数:

    const Rational operator*(const Rational& lhs
                             const Rational& rhs)
    {
        Rational result(las.n*rhs.n , lhs.d*rhs.d);
        return result;  //返回一份copy
    }
    

    经过之前学习,我们知道这样开销较大。

    现在考虑考虑返回引用的版本,即将细节改成 return &result; ,并将返回类型改成const Rational&

    这有严重问题,不用new来构造对象的话,对象只是一个local本地对象,它将在函数退出后被销毁。这会导致你得到的引用指针将会指向一个不明的「残骸」

    看看另一种版本,由new构造的对象储存在heap堆上:

    const Rational& operator*(const Rational& lhs
                              const Rational& rhs)
    {
        Rational* result = new result(las.n*rhs.n , lhs.d*rhs.d);
        return* result;  //返回指针,被&加工为产量指针
    }
    

    没有啥卵用,因为new的过程还是要构造对象。其实这个版本更糟,因为你需要考虑delete。

    还有一种坑爹的版本,就是将函数内部的Rational对象声明为静态的,并返回它的引用。这里虽然解决了被销毁的问题,但是对于C++多线程它是不安全的。

    假设我们已经写好了==重载函数,且完全正确:

    bool operator==(const Rational& lhs, const Rational& rhs);

    假设有下面的操作:

    Rational a,b,c,d;
    ...
    if((a*b)==(c*d)){...}
    else ...
    

    估计你也想到了,两个*运算都返回一个指向同一处static对象地址的引用,所以这个式子的比较结果永远为true。

    抱歉,说了这么多,我们还是回到了起点————对于*运算的重载,我们几乎只能采用返回一个新对象的方法:

    //第一个版本的精简
    const Rational operator*(const Rational& lhs
                             const Rational& rhs)
    {
        
        return Rational(las.n*rhs.n , lhs.d*rhs.d);  
        //返回一份copy
    }
    

    总结:

    • 绝不要返回指向local stack对象的pointer或reference / 返回指向heap-allocated对象的reference / 返回指向local static对象的pointer或reference,而且可能同时需要多个这样的对象

    条款22: 将成员变量声明为private

    这个建议适用于protected成员

    1. 首先,获取私有成员的渠道大部分是函数,所以客户访问成员不需要考虑究竟是否要加小括号,因为全是函数,他们照做就是。

    2. 其次,你可以通过函数精确控制各种访问权限:

    class AccessLevels{
    private:
        int noAccess; //无任何访问动作
        int readOnly; //read-only access
        int readWrite; //read-write access
        int writeOnly; //write-only
    public:
        ...
        int getReadOnly() const { return readOnly; }
        void setReadWrite(int v) { readWrite = v; }
        int getReadWrite() const { return readWrite; }
        void setWriteOnly(int v) { writeOnly = value; }
    };
    

    一般来说,每个成员变量都需要getter和setter的情况实属罕见,所以这样的控制很有必要。

    将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。

    现在我问你,protected成员的封装性是否高于public?答案是不尽如人意。

    我们知道,public的访问一般要求客户自己写代码来实现,一旦public的成员函数被取消,所有使用它的客户代码都会被破坏。而protected被取消掉的话,它的所有dervied classes都会被破坏。因此protected和public一样缺乏封装性。

    所以从封装的角度来看,其实只有两种访问权限:private(提供封装)和其它(不提供封装)。


    条款23:宁以non-member、non-friend替换member函数

    假设有一个Class代表网页浏览器。有几个成员函数,提供了清除缓存、清除历史记录、清除cookies:

    class WebBrowser{
    public:
        ...
        void clearCache();
        void clearHistory();
        void removeCookies();
        void clearEverything(); //调用上述三个函数。
        ...
    };
    

    这些功能也可由一个non-member函数实现,只需传入一个WebBrowser对象引用就行:

    void clearBrowser(WebBrowser& wb)
    {
        wb.clearCache();
        wb.clearHistory();
        wb.removeCookies();
    }
    

    那么,哪一个比较好呢?member函数clearEverything还是non-member clearBrowser?

    根据面向对象守则,数据以及操作数据的函数应捆绑在一块儿,这意味member函数是更好的选择。然而这是一个误解。

    和你直觉相反的是,non-member函数clearBrowser封装性实际上比member版本的clearEverything还要高。

    通常来说,member函数不仅能访问Class里的private成员,还能取用enums、typedefs等。我们说高封装性是指应有尽可能少的代码能够直接「看到」私有成员变量,这时non-member函数的优越性就体现出来了,能完成同样的机能,但又和Class的私有成员保持了绝对的距离。

    所以如果只考虑封装性的话,选择的关键在于member和non-member、non-friend之间。(friend的权限和member一样大)

    我们甚至可以将函数clearBrowser作为某工具类的一个static member函数,给其它Class用时,再变成non-member。

    在C++,你以后可能比较自然的做法是,将clearBrowser成为一个non-member函数并位于WebBrowser所在同一个namespace里:

    namespace WebBrowserStuff{
        class WebBrowser{...};
        void clearBrowser(WebBrowser& wb);
    }
    

    条款24: 若所有参数都需类型转换,请采用non-member函数

    令class支持隐式转换通常会有风险。但常见的例外是建立「数值类型」。假设我们又设计一个有理数Class,允许整数“隐式转换”为有理数很合理。假设我们这样构造有理数Class:

    class Rational{
    public:
        Rational(int numerator = 0,
                 int denominator = 1);   
        //刻意不为explicit,允许int-to-Rational隐式转换
        int numerator() const;   //分子访问
        int denominator() const;   //分母访问
    private:
        ...
    };
    

    假设此时你想让Class支持算术运算,比如让它能作乘法运算。你不确定要用member、non-member还是non-member friend函数。你的直觉告诉你要用member版本的operator*重载:

    class Rational{
    public:
    ...
    const Rational operator*(const Rational& rhs) const;
    };

    这个设计能让相乘很自然:

    Rational oneEight(1,8);
    Rational oneHalf(1,2);
    Rational result = oneHalf * oneEight;
    result = result * oneEight;

    你不满足,你希望Rationals能和ints相乘:

    result = oneHalf * 2; //Good
    result = 2 * oneHalf; //Bad!

    Wait,乘法应该满足交换律啊!

    问题出在哪?我们翻译一下上述代码:

    result = oneHalf.operator*(2);  //Good
    result = 2.operator*(oneHalf);  //Bad!
    

    语句一中,将int型2传入操作符函数后,发生了隐式转换(原参数是一个Rational引用)。有点类似于:

    const Rational temp(2); //编译器建立一个临时对象
    result = oneHalf.operator*(temp); //传参

    这里成功的原因是我们没有将构造函数声明为显式的,这为上面的操作提供了支持。

    然而第二个语句呢?2作为一个int型,并没有class,更别说operator* 成员函数。编译器会试着在namespace或global域内寻找是否有一个non-member operator*。然而并没有。

    当然,如果构造函数是explicit,没有一个语句会通过编译。

    结论是,当参数位于参数列(parameter list)内,才有资格参与隐式转换。这就是为啥第一个语句能够通过编译。

    但是我们想支持混合运算啊喂!!也就是能让Rational和其它类型数据相运算!!

    现在考虑non-member operator* :

    class Rational{
    ...
    };
    const Rational operator*(const Rational& lhs,const Rational& rhs)
    {
    return Rational(lhs.numerator()*rhs.numerator(),
    lhs.denominator()*rhs.denominator());
    } //变成non-member函数

    执行:

    Rational oneFourth(1,4);
    Rational result;
    result = oneFourth * 2;
    result = 2 * oneFourth;     //全部通过编译,恭喜!!!!
    

    这都很好。但要不要考虑将operator* 变为一个friend函数呢?答案是否定的。因为我们可以从上面的操作中看出,完全可以只靠Rational的public接口完成operator* 的任务。这导出一个重要的观察:

    member函数的方面就是non-member,而不是friend。

    无论何时,可以避免使用friend就避免。

    必须告诉你的是,这些不是「真理」。因为从Object-Oriented C++跨入Template C++后,你会考虑将Rational设计为一个class template而非class,这将引入很多新考虑,以后会提到。

    Remember :

    • 若你要为某函数的所有参数(包括this隐指针所指参数)进行类型转换,这个函数必须设计为non-member

    条款25: 考虑写出不抛异常的swap函数

    swap原本是STL一部分,实现了两个数据对象的交换。后来成为异常安全性编程的脊柱(exception-safe programming)。

    以下是swap在标准程序库中的典型实现:

    namespace std{
        template<typename T>
        void swap(T& a,T& b)
        {
            T temp(a);
            a = b;
            b = temp;
        }
    }
    //只要T类型支持copying(copy构造函数和=赋值符),以上代码即可帮你自动置换。
    

    这种default swap实现很平庸,特别对于某些类型,它的效率会显得较低。

    现在我们讨论这种类型,也就是“以指针指向一个对象,内含真正数据”的类型,也就是“pimp”手法(pointer to implementation)。

    现在我们试着用pimp来设计一个Widget class:

    class WidgetImpl{  //Widget类的数据实现
    public:
        ...
    private:
        int a, b, c;
        std::vector<double> v; //可能会有很多数据,复制时间长
        ...
    };
    
    class Widget{  //使用pimp手法
    public:
        Widget(const Widget& rhs);
        Widget& operator=(const Widget& rhs) //复制Widget时,令它复制其WidgetImpl对象
        {
            ...
            *pImpl = *(rhs.pImpl);
            ...
        }
        ...
    private:
        WidgetImpl* pImpl;//指向对象内含Widget数据
    };
    

    如果我们交换两个Widget时,只希望置换其中的pImpl指针;然而默认的swap算法不知道。在swap的三条置换语句中,不只复制了三个Widget,还复制三个WidgetImpl对象。这很缺乏效率,一点不令人兴奋!

    所以我们应告诉swap该怎么做:将 std::swap 针对Widget特化。下面进行基本构思,但是暂时通不过编译:

    namespace std{
        template<>
        void swap<Widget>(Widget& a,Widget& b) //"T为Widget"的特化版本
        {
            swap(a.pImpl, b.pImpl); //仅置换Widgets内部指针
        }
    }
    

    First,此函数开头 template<> 表明它是std::swap的一个全特化(total template specialization)版本。 函数名后的 <Widget> 表明这一特化版本系针对”T是Widget”而设计。 所以当你将swap施行于Widget对象身上便会自动调用此版本。

    我们通常不被允许改变std空间里的任何东西,但被允许为标准templates(比如此处的swap)制造特化版本。

    之前说通不过编译的原因是,pImpl指针是私有的。可以考虑将此特化版本声明为friend;但和以往规矩不同,这次我们在Widget内部声明一个swap的公共函数进行真正的置换工作,再特化 std::swap ,令它调用该member function:

    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);
        }
    }
    

    实际上,这也是类似STL容器的写法,它们都提供public swap成员函数和std::swap特化版本。

    另一种情况:假设Widget和WidgetImpl都是class templates而非classes:

    template<typename T>
    class WidgetImpl { ... };
    
    template<typename T>
    class Widget { ... };
    

    在Widget内写一个swap函数依旧简单,但是特化 std::swap 时会遇到麻烦:

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

    这么写不合法的原因是,我们正企图偏特化(partially specialize)一个function template(std::swap)。然而C++仅允许对class templates偏特化。

    所以惯常做法是手动添加一个重载的版本:

    namespace std{
        template<typename T>
        void swap(Widget<T>& a, Widget<T>& b)//注意"swap"后没有"<...>"
        { a.swap(b); }  //其实这也不合法,稍后提出
    }
    

    在C++中,重载function templates没问题。然而std是特殊的命名空间,C++标准委员会禁止膨胀已经写好的东西,因为可能会发生不明行为。所以问题出在我们的重载版本正在做这样的事。

    绕了一大圈,我们没有前功尽弃。要提供一个高效的template swap特定版本,可以声明一个non-member swap让它调用member swap,而不再特化 std::swap 或在std里重载它。

    为了简化,将Widget相关机能一并置入命名空间WidgetStuff内:

    namespace WidgetStuff{
        ...           //模版化的WidgetImpl等等
        template<typename T>
        class Widget { ... };   //内含swap成员函数
        ...
        template<typename T>
        void swap(Widget<T>& a, Widget<T>& b)
        {
            a.swap(b);
        }
    }
    

    从现在开始,任何地点的代码若打算置换俩Widget对象而调用swap。C++的名称查找法则(name lookup rules)会找到WidgetStuff空间内的Widget专属豪华版本。

    以上做法适用于classes和class templates。不幸的是,有一种情况使我们不得不为classes特化 std::swap ———只要你想让你的专属swap能在尽可能多语境被调用,你需要写一个该class命名空间内的non-member版本和一个 std::swap 特化版本。(稍后解释)

    < 事实上你可以不采用namespace的方式,但global空间里漫天飞的东西真的好看吗?>

    现在开始解释: 假设你在写一个function template,需置换两个对象值:

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

    该调用哪种swap呢,也许有一种可能存在的T专属版本此时栖身于某namespace中?(当然不可以在std内) 所以你希望如果存在专属版本就调用它;不存在就用默认的 std::swap 吧:

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

    之后C++会在global域和T所在namespace里搜索可能存在的T专属版swap,若没有则调用默认 std::swap

    这里有一个小trick,如果你这么写: std::swap(obj1, obj2); ,语意会截然不同,这相当于强迫使用std内的swap ————— 你get到了吗,这就是我们要写特化std::swap 的动机!这使得类型专属的swap实现也能被这些笨笨的代码所用。


    OVER

  • 相关阅读:
    解决ValueError: Some of types cannot be determined by the first 100 rows,
    关于number_format的一个报错
    关于IPV6审核被拒问题处理
    项目集成三方库由静态库转为动态库遇到的问题总结
    Swift判断对象属于什么类型
    运行项目报错 报scandir() has been disabled for security reasons
    CocoPods port 443
    Jetpack学习-WorkManager
    Jetpack学习-Paging
    Jetpack学习-Navigation
  • 原文地址:https://www.cnblogs.com/1Kasshole/p/9096980.html
Copyright © 2011-2022 走看看