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-IP合法性_Java

    描述 现在IPV4下用一个32位无符号整数来表示,一般用点分方式来显示,点将IP地址分成4个部分,每个部分为8位,表示成一个无符号整数(因此不需要用正号出现),如10.137.17.1,是我们非常熟悉 ...

  2. Json字符与Json对象的相互转换

    Json字符与Json对象的相互转换方式有很多,接下来将为大家一一介绍下,感兴趣的朋友可以参考下哈,希望可以帮助到你 1>jQuery插件支持的转换方式: 复制代码 代码如下: $.parseJ ...

  3. 在 Eclipse 中使用 JSHint 检查 JavaScript 代码

    JSHint Home: http://www.jshint.com/ JSHint Options: http://www.jshint.com/options/ JSHint For Eclips ...

  4. 转 Java中Filter、Servlet、Listener的学习

      1.Filter的功能filter功能,它使用户可以改变一个 request和修改一个response. Filter 不是一个servlet,它不能产生一个response,它能够在一个requ ...

  5. 循环/loop 结构/structure

    1.Shell loop 2.C++/CPlusPlus ①.std::for_each ②.for loop ③.Iterator library 3.Python Loop ①.Python.or ...

  6. ThinkPHP3.2 加载过程(三)

    上次回顾: IS_CGI ,IS_WIN,IS_CLI,MAGIC_QUOTES_GPC干嘛用 IS_WIN 看了一下后面的代码  基本上就是为了保证在不同环境下运行时,由于有些操作系统会对文件路径大 ...

  7. C语言使用中的细节问题总结

    1.在使用bool关键字时,出现"error:'bool' undeclared(first use in this function)"的错误,原因为C语言本身是没有bool关键 ...

  8. 【HeadFirst设计模式】13.与设计模式相处

    模式: 是在某情境下,针对某问题的某种解决方案. 要点: 让设计模式自然而然地出现在你的设计中,而不是为了使用而使用. 设计模式并非僵化的教条,你可以依据自己的需要采用或者进行调整. 总是使用最简单的 ...

  9. global, $GLOBALS[]

    // global在函数中产生一个指向函数外部变量的别名变量,而不是真正的函数外部变量,一旦改变了别名的变量指向地址,就会发生一些意外的情况 $a = 10; function test() { gl ...

  10. Delphi新语法和ifthen的扩展联想

    Delphi之前已经重载了好多个ifthen函数 Math单元 ): Integer; overload; inline; ): Int64; overload; inline; ): UInt64; ...