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. HW-找7(测试ok满分注意小于等于30000的条件)

    输出7有关数字的个数,包括7的倍数,还有包含7的数字(如17,27,37...70,71,72,73...)的个数 知识点 循环 运行时间限制 0M 内存限制 0 输入 一个正整数N.(N不大于300 ...

  2. [译]JavaScript insertAdjacentHTML

    原文地址:http://davidwalsh.name/insertadjacenthtml-beforeend 该死的DOM慢的很.随着我们的网站动态交互和Ajax操作越来越多,我们需要寻找一种高性 ...

  3. web前端面试题收集(一)

    CSS中margin和padding的区别? Javascript中如何检测一个变量是一个String类型?请写出函数实现. 网页中实现一个计算当年还剩多少时间的倒计时程序,要求网页上实时动态显示“x ...

  4. OpenJudge/Poj 2027 No Brainer

    1.链接地址: http://bailian.openjudge.cn/practice/2027 http://poj.org/problem?id=2027 2.题目: 总Time Limit: ...

  5. 第28条:利用有限制通配符来提升API的灵活性

    参数化类型是不可变的.对两个不同类型T1和T2而言,List<T1>与List<T2>没有父子类型关系. 考虑: public class Stack<E> { p ...

  6. windows 安装 setuptools

    在python的网站上 : https://pypi.python.org/pypi/setuptools/ 查找windows,显不如下: 点击 ez_setup.py进入, 并将内容复制下来, 保 ...

  7. Nginx+keepalived实现负载均衡

    Nginx的优点是: 1.工作在网络的7层之上,可以针对http应用做一些分流的策略,比如针对域名.目录结构,它的正则规则比HAProxy更为强大和灵活,这也是它目前广泛流行的主要原因之一,Nginx ...

  8. QML鼠标区域控制

    鼠标操作使用很多,下面给出一个示例: import QtQuick 2.4 import QtQuick.Controls 1.3 import QtQuick.Window 2.2 import Q ...

  9. DataGridView添加另外一个控件。

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; u ...

  10. linux(centos)搭建svn

    1.yum install subversion 2.输入rpm -ql subversion查看安装位置 输入 svn --help可以查看svn的使用方法 3.创建svn版本库目录 mkdir - ...