[译]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/

posted @ 2013-11-23 12:44  Navono  阅读(253)  评论(0编辑  收藏  举报