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

Problem

Guru Question

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

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

class polygon {
public:
polygon() : area{-} {} void add_point( const point pt ) { area = -;
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 < ) // if not yet calculated and cached
calc_area(); // calculate now
return area;
} private:
void calc_area() {
area = ;
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 = ; i < last; ++i ) // concatenate
ret.add_point( rhs.get_point(i) );
return ret;
} void f( const polygon& poly ) {
const_cast<polygon&>(poly).add_point( {,} );
} void g( polygon& const poly ) { poly.add_point( {,} ); } void h( polygon* const poly ) { poly->add_point( {,} ); } 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 = ; // 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 < ) // 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 = ;
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 < ) // 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 = ; 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( {,} );
}

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

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

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

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

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

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

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{-} {} polygon( const polygon& other ) : points{other.points}, area{-} { } polygon( polygon&& other )
: points{move(other.points)}, area{other.area.load()}
{ other.area = -; } polygon& operator=( const polygon& other )
{ points = other.points; area = -; return *this; } polygon& operator=( polygon&& other ) {
points = move(other.points);
area = other.area.load();
other.area = -;
return *this;
} void add_point( point pt )
{ area = -; 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 < ) // 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 = ; i < last; ++i ) // concatenate
lhs.add_point( rhs.get_point(i) );
return lhs;
} void f( polygon& poly ) { poly.add_point( {,} ); } void g( polygon& poly ) { poly.add_point( {,} ); } void h( polygon* poly ) { poly->add_point( {,} ); } int main() {
auto poly = polygon{}; f(poly);
g(poly);
h(&poly);
}

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

[译]GotW #6b Const-Correctness, Part 2的更多相关文章

  1. [译]GotW #6a: Const-Correctness, Part 1

    const 和 mutable在C++存在已经很多年了,对于如今的这两个关键字你了解多少? Problem JG Question 1. 什么是“共享变量”? Guru Question 2. con ...

  2. [译]GotW #4 Class Mechanics

    你对写一个类的细节有多在行?这条款不仅注重公然的错误,更多的是一种专业的风格.了解这些原则将会帮助你设计易于使用和易于管理的类. JG Question 1. 什么使得接口“容易正确使用,错误使用却很 ...

  3. [译]GotW #3: Using the Standard Library (or, Temporaries Revisited)

    高效的代码重用是良好的软件工程中重要的一部分.为了演示如何更好地通过使用标准库算法而不是手工编写,我们再次考虑先前的问题.演示通过简单利用标准库中已有的算法来避免的一些问题. Problem JG Q ...

  4. [译]GotW #2: Temporary Objects

        不必要的和(或)临时的变量经常是罪魁祸首,它让你在程序性能方面的努力功亏一篑.如何才能识别出它们然后避免它们呢? Problem JG Question: 1. 什么是临时变量? Guru Q ...

  5. [译]GotW #89 Smart Pointers

    There's a lot to love about standard smart pointers in general, and unique_ptr in particular. Proble ...

  6. [译]GotW #1: Variable Initialization 续

    Answer 2. 下面每行代码都做了什么? 在Q2中,我们创建了一个vector<int>且传了参数10和20到构造函数中,第一种情况下(10,20),第二种情况是{10, 20}. 它 ...

  7. [译]GotW #1: Variable Initialization

    原文地址:http://herbsutter.com/2013/05/09/gotw-1-solution/ 第一个问题强调的是要明白自己在写什么的重要性.下面有几行简单的代码--它们大多数之间都有区 ...

  8. [译]GotW #5:Overriding Virtual Functions

       虚函数是一个很基本的特性,但是它们偶尔会隐藏在很微妙的地方,然后等着你.如果你能回答下面的问题,那么你已经完全了解了它,你不太能浪费太多时间去调试类似下面的问题. Problem JG Ques ...

  9. Meaning of “const” last in a C++ method declaration?

    函数尾部的const是什么意思? 1 Answer by Jnick Bernnet A "const function", denoted with the keyword co ...

随机推荐

  1. wpf打印控件 实现分页打印控件功能

    因为 要实现打印 wpf  listbox控件  数据特别多 要打印在 几张纸上    找了几天 都没有找到相关的例子 现在 解决了 在这里和大家分享一下 public void print(Fram ...

  2. wamp优化

    友情链接:IT狂人博客 转载请注明作者:浮沉雄鹰 和本文链接:http://www.cnblogs.com/xby1993/p/3342085.html 一.修改php.ini, 修改上传文件大小限制 ...

  3. Dorado事件的参数

    onClick,onSuccess 事件一般只有2个参数(self,arg),其实参数是可以添加的.可以把控件的ID直接放到参数里面来,然后在事件编辑里直接通过ID作控件对象,直接设值就好了,不要输入 ...

  4. (int)、(int&)和(int*)的区别(转)

    (1).首先通过一个例子看(int)和(int&)的区别: float a = 1.0f;cout << (int)a << endl;cout << (i ...

  5. 安装Cygwin

    如果你现在正在学习C语言,而你又不希望使用微软提供的任何C语言的任何编译器,那么你应该考虑一下GCC.GCC是运行于类UNIX系统下的编译器工具集,这又引出了另一个让人头疼的问题,你没有一台现成的装有 ...

  6. CIFS与NFS(转)

    1.CIFS Microsoft推出SMB(server message block)后,进一步发展,使其扩展到Internet上,成为common internet file system. CIF ...

  7. JavaScript 继承的几种模式

    /** * Created by 2016 on 2016/6/5. */ //1.原型链继承 //把子类的原型,定义为超类的实例 通过原型来访问超类的方法和属性 function Person(){ ...

  8. php四种基础排序算法的运行时间比较

    /** * php四种基础排序算法的运行时间比较 * @authors Jesse (jesse152@163.com) * @date 2016-08-11 07:12:14 */ //冒泡排序法 ...

  9. mysql 数据库备份,恢复。。。。

    mysql 数据备份,恢复,恢复没写,这里只写了备份... 先暂作记录吧! 备份:表结构和数据完全分开,默认有一个文件会记录所有表的结构,然后表中数据的备份 如果超过分卷的大小则会分成多个文件,不然则 ...

  10. Centos系统mysql 忘记root用户的密码

    Centos系统mysql 忘记root用户的密码: 第一步:(停掉正在运行的mysql) [root@maomao ~]# /etc/init.d/mysqld stop Stopping MySQ ...