zoukankan      html  css  js  c++  java
  • [译]GotW #6b Const-Correctness, Part 2

         const和mutable对于书写安全代码来说是个很有利的工具,坚持使用它们。

    Problem

    Guru Question

    在下面代码中,在只要合适的情况下,对const进行增加和删除(包括一些微小的变化和一些相关的关键字)。注意:不要注释或者改变程序的结构。这个程序只作为演示用途。

    另外:程序的哪些地方是由于错误地使用const而导致的未定义行为或不可编译?

    class polygon {
    public:
        polygon() : area{-1} {}
    
        void add_point( const point pt ) { area = -1;
                                           points.push_back(pt); }
    
        point get_point( const int i ) { return points[i]; }
    
        int get_num_points() { return points.size(); }
    
        double get_area() {
            if( area < 0 )   // if not yet calculated and cached
                calc_area();     // calculate now
            return area;
        }
    
    private:
        void calc_area() {
            area = 0;
            vector<point>::iterator i;
            for( i = begin(points); i != end(points); ++i )
                area += /* some work using *i */;
        }
    
        vector<point> points;
        double        area;
    };
    
    polygon operator+( polygon& lhs, polygon& rhs ) {
        auto ret = lhs;
        auto last = rhs.get_num_points();
        for( auto i = 0; i < last; ++i ) // concatenate
            ret.add_point( rhs.get_point(i) );
        return ret;
    }
    
    void f( const polygon& poly ) {
        const_cast<polygon&>(poly).add_point( {0,0} );
    }
    
    void g( polygon& const poly ) { poly.add_point( {1,1} ); }
    
    void h( polygon* const poly ) { poly->add_point( {2,2} ); }
    
    int main() {
        polygon poly;
        const polygon cpoly;
    
        f(poly);
        f(cpoly);
        g(poly);
        h(&poly);
    }

    Stop and thinking….

    Solution

         当我提出这类问题的时候,我发现大多数人认为这个问题很容易,并且通常解决的只是一般的const问题。但是这里面有很多细微的差别我们应该知道,所有有了这篇blog

    1.point对象按值传递,因此这里声明为const有一点点好处

    void  add_point( const point pt )

         在这种特殊情况下,因为函数定义为inline,这里的const值参数(value parameter)就变得有意义了。这是因为inline函数的声明和定义是在同一处,否则,const值参数只应该出现在定义中,而不是声明中。让我们来看看为什么。

         在函数声明中,往值参数中添加const对于函数来说是无关重要的,它对于调用者来说毫无意义且常常会起到迷惑作用。对于编译器来说,函数的签名不管是否在值参数前加入const都是相同的。

    // value parameter: top-level const is not part of function signature
    int f( int );
    int f( const int );    // redeclares f(int): this is the same function
    
    // non-value parameter: top-level const is part of function signature
    int g( int& );
    int g( const int& );   // overloads g(int&): these are two functions

         在值参数前加const的确会影响到它在函数体内的实际定义。记住,在函数体内,形参只是第一组局部变量。因此在值参数前加const仅仅意味着在函数内不能修改这个局部变量,这个只发生在参数上。下面是一个例子。

    int f( int );          // declaration: no const
    
    int f( const int i ) { // definition: use const to express "read-only"
    
        vector<int> v;
        v.push_back(i);    // ok, only reads from i
    
        i = 42;            // error, attempts to modify i
    
    }

           Guideline:在向前声明一个函数时,不要再传值参数前加入const。你可以在定义处加上const来表达一个只读参数。


    2.get_point和get_num_points应该是const

    point get_point( const int i ) { return points[i]; }
    
    int   get_num_points() { return points.size(); }

        以上函数应该被标识为const,因为他们没有改变对象的状态。

    3.get_area应该是const

    double get_area() {
            if( area < 0 )   // if not yet calculated and cached
                calc_area();     // calculate now
            return area;
        }

         尽管这个函数在内部修改了对象的内部状态,我们也应该考虑将它标识为const,为什么?因为这个函数没有修改这个对象的可观察状态(observable state),我们只是在这做了写缓存动作,这只是内部的一些实现细节。这个对象在逻辑上依然是const,尽管它在物理上(physically)不是。

    4.根据3,calc_area也应该是const

    void calc_area() {
            area = 0;
            vector<point>::iterator i;
            for( i = begin(points); i != end(points); ++i )
                area += /* some work using *i */;
        }

         一旦我们把get_area标识为const,这个私有的辅助函数也应该是const的,反过来说,一旦将这个函数标识为const,编译器就会告知你同样应在成员变量area上做出改变:

         · 声明为mutable,这样它在const函数中就具有可写性(writable)
         · 使用mutex或使之为atomic<>来同步,这样就具有并发安全性,像GotW #6a中讨论的那样。

    5.同样,calc_area应该使用const_iterator
         迭代器不应该改变points集合的状态,因此它应该是const_iterator。如果我们将calc_area标识为const成员函数的话,那我们无论如何都会做出这个改变。但是有一点要注意的是,如果我们在for中为迭代器使用auto的话,那么我们在这个上可以完全不做改变。当我们在cal_area内做for循环时,我们应该优先使用range-based for循环,同样包括auto.
    组合上述所说,我们得到了下面的代码:

    for( auto& pt : points )
              area += /* some work using pt */;

             Guidline: 优先使用auto来声明变量。

             Guideline: 当要顺序访问集合元素时,优先使用rang-based for循环。

    6.area应该是mutable和同步的

    double        area;

         像上述所说,联合其他内部的变化,这个内部缓存变量area应该是mutable的,这样就可以在const成员函数中被安全和正确地使用,同时因为它是潜在的共享变量,那么就可能被多个const操作并发执行,因此它必须是同步的,使用mutex或使之为atomic。

    额外提问:在继续阅读之前,它应该是:使用mutex来保护,还是使之为atomic<double>?

    你有考虑过吗?我们继续...

    上述两者都行,但是使用mutex对于单个变量来说有点过度(overkill)。

    选项1是使用mutex,可能很快成为标准的"mutable mutex mutables"模式

    // Option 1: Use a mutex 
    
        double get_area() const {
            auto lock = unique_lock<mutex>{mutables};
            if( area < 0 )   // if not yet calculated and cached
                calc_area();     // calculate now
            return area;
        }
    
    private:
        // ...
        mutable mutex  mutables;      // canonical pattern: mutex that
        mutable double area;          //   covers all mutable members

         如果在未来要增加更多的数据成员的话,选项1会表现的不错。如果你在未来增加更多的使用了area变量的const成员函数的话,那么这个选项就变得很具有入侵性且变得不那么好了。因为在const成员函数内部应该在使用area之前在mutex请求锁。

         选项2只是将double变成mutable atomic<double>。这个是很吸引人的,因为polygon的"mutable"部分只是一个单一变量。它能达到要求,但是你必须小心,因为这不是唯一必要的改变,原因有二:

          · 次要原因是atomic<double>不支持+=操作。因此我们只是改变area的类型的话,calc_area是不会编译通过的。这有个变通方案,但也导致了主要原因。
          · 主要原因是,因为calc_area是个组合操作,且必须能安全运行在多线程并发的情况下,我们必须重构calc_area函数,让它能够安全地并发执行。特别是它不应该执行完一次操作立马更新area,同时要确保多个并发竞争跟新area不会引起覆盖导致写入的值丢失。

    有几个方法来达到上述要求,但是最简单的可能是在并发调用calc_area的情况下允许良性的冗余再计算。因为它不可能比阻塞并发调用(无论如何都必须等待)更差。

    // Option 2: Use an atomic
    
        void calc_area() const {
            auto tmp = 0.0;              // do all the work off to the side
            for( auto& pt : points )
                tmp += /* some work using pt */;
            area = tmp;                  // then commit with a single write
        }
    
    private:
        // ...
        mutable atomic<double> area;

         需要注意的是,调用calc_area的并发const操作依然会重叠和覆盖相互间的结果。但它是良性的,因为这些操作是并发的const操作,因此它们全部计算相同的值。同样,在并发的calc_area调用的循环中使用共享points变量,这会使得我们考虑检查它不会导致缓存竞争,因为这些都是读操作,所以不会。

    7.operator+的rhs参数应该是const引用

    polygon operator+( polygon& lhs, polygon& rhs ) {

    rhs参数应该是const引用。


             Guideline:如果你只是准备进行读取(而不是拷贝),那么优先使用只读参数,通过const&。

    对于lhs:

    8.operator+的lhs应该是传值

         这个关键部分是我们无论如何都要对它进行拷贝:

    auto ret = lhs;

         当你处在“无论如何都要对一个只读参数进行拷贝”的特殊情况下,有几种方式可以接受这样的参数,我会在其他GotW中详细讨论其中的细节。但是对于现在的情况来说,不需要考虑的太多,简单地使用传值就足够了。其中有些优点我们已经在GotW #4中讨论过了。

         · 如果调用方传入一个命名的polygon对象(一个左值),这不会有区别。传const引用紧随其后是一个显式的拷贝,传值将会执行一次拷贝
         · 如果调用方传入的是一个临时polygon对象(一个右值),编译器会自动地移动构造(move-constructs)lhs,对于一些小的类型来说可能不会有太大区别,比如polygon,但是对于其他类型来说却是相对“便宜”的

                 Guideline: 如果无论如何都需要对参数进行拷贝,优先使用传值参数。因为它可以从rvalue参数进行移动操作。

    9.在operator+中,last应该是const

    auto last = rhs.get_num_points();
        for( auto i = 0; i < last; ++i ) // concatenate
            ret.add_point( rhs.get_point(i) );
        return ret;
    }

    因为last不应该被改变,所以可是使之为const

       

             Guideline:如果变量不会被改变,那么优先选择使这些变量为const,包括局部变量。

    顺便说一下,一旦我们把rhs改变成const引用,我们也能明白为什么get_point变为const成员函数的另一个原因。

    10.f的const_cast可能会导致未定义行为

    void f( const polygon& poly ) {
        const_cast<polygon&>(poly).add_point( {0,0} );
    }

         如果引用的对象声明为const的话,那么const_cast的结果是未定义的。就像在f(cpoly)这种情况。

         这个参数不是真正的const,所以没有声明为const,接着试图去修改它。这是在欺骗编译器,可能对于调用者来说没有关系,但是个坏主意。

    11.g的const是非法且无用的

    void g( polygon& const poly ) { poly.add_point( {1,1} ); }

         这个const是非法的:不能直接将const应用在引用本身,除了引用本身已经是const,因为它们不能不能被复位去引用到另一个对象。

    void h( polygon* const poly ) { poly->add_point( {2,2} ); }

         h的const仅仅只是确保在h函数体内不会修改指针。和add_pont与get_point的const参数是一样的。

    12.检查主程序

    int main() {
        polygon poly;
        const polygon cpoly;
    
        f(poly);

    没问题。

    f(cpoly);

    就像上面说的那样,当f试图去擦除参数的常量性后修改其值会导致未定义的结果。

    g(poly);


    没问题。

    h(&poly);

    没问题。

    Summary

    下面是一个修改后的版本。不要试图去修改任何的差的代码风格。因为现在修改成了atomic成员,它是不可拷贝的(copyable),所以现在提供了一个copy和move操作。

    class polygon {
    public:
        polygon() : area{-1} {}
    
        polygon( const polygon& other ) : points{other.points}, area{-1} { }
    
        polygon( polygon&& other ) 
            : points{move(other.points)}, area{other.area.load()}
            { other.area = -1; }
    
        polygon& operator=( const polygon& other )
            { points = other.points; area = -1; return *this; }
    
        polygon& operator=( polygon&& other ) {
            points = move(other.points);
            area = other.area.load();
            other.area = -1;
            return *this;
        }
    
        void add_point( point pt ) 
            { area = -1; points.push_back(pt); }
    
        point get_point( int i ) const { return points[i]; }
    
        int get_num_points() const { return points.size(); }
    
        double get_area() const {
            if( area < 0 )   // if not yet calculated and cached
                calc_area();     // calculate now
            return area;
        }
    
    private:
        void calc_area() const {
            auto tmp = 0.0;
            for( auto& pt : points )
                tmp += /* some work using pt */;
            area = tmp;
        }
    
        vector<point>          points;
        mutable atomic<double> area;
    };
    
    polygon operator+( polygon lhs, const polygon& rhs ) {
        const auto last = rhs.get_num_points();
        for( auto i = 0; i < last; ++i ) // concatenate
            lhs.add_point( rhs.get_point(i) );
        return lhs;
    }
    
    void f( polygon& poly ) { poly.add_point( {0,0} ); }
    
    void g( polygon& poly ) { poly.add_point( {1,1} ); }
    
    void h( polygon* poly ) { poly->add_point( {2,2} ); }
    
    int main() {
        auto poly = polygon{};
    
        f(poly);
        g(poly);
        h(&poly);
    }

    原文链接:http://herbsutter.com/2013/05/28/gotw-6b-solution-const-correctness-part-2/

  • 相关阅读:
    VOL.1 利用vmware ThinApp 制作非XP下可以运行的IE6 【无插件版】(windows vista/7/8 x86/x64 )
    js中each的用法
    Math函数floor、round、ceil的区别
    JS中的prototype解析
    Ubuntu虚拟机搭建
    我的第一篇blog
    实验四+124+高小娟
    实验三+124+高小娟
    第5次作业+105032014124+高小娟
    第4次作业类测试代码+105032014124+高小娟
  • 原文地址:https://www.cnblogs.com/navono007/p/3438865.html
Copyright © 2011-2022 走看看