mutable
mutable
的中文意思是易变的,是C++的一个关键字。它的作用就是允许修改被const修饰的成员变量或在const修饰的成员函数中修改成员变量。
常用场景
什么情况下我们会使用到mutable?
一般我们会用const
修饰get类的函数,明确函数内部不会修改任何成员变量,但是如果内部的成员变量需要多线程读写,我们必须加锁保证多线程安全。毫无疑问,加锁操作是要修改锁对象的,此时就用到了mutable
。
#include <mutex>
class A
{
public:
int get() const
{
std::lock_guard<std::mutex> lk( m );
return value;
}
void set( int v )
{
std::lock_guard<std::mutex> lk( m );
value = v;
return;
}
private:
mutable std::mutex m; // 不加mutable,编译就会报错
int value;
};
int main()
{
A a;
return;
}
还有比如我们使用std::set
保存对象,当我们修改对象中非key的值时也需要用到mutable
,不然就得重新构造对象,设置非key的值,再重新塞入std::set
中。
#include <set>
class A
{
public:
void set( int v ) const
{
value = v;
return;
}
bool operator<( const A& rhs ) const
{
return key < rhs.key;
}
private:
int key;
mutable int value; // 不加mutable,编译就会报错
};
int main()
{
std::set<A> s;
s.insert(A());
s.begin()->set(10);
return 0;
}
std::set
的迭代器类型总是const_iterator
。这很好理解,因为对象的在std::set
中的位置是根据其key值确定的,如果允许修改,那么std::set
结构就不对了,再加上编译器无法确定函数内是否会修改key,所以通过其迭代器对象操作的函数必须是const
修饰的。同理,std::map
的first
也是如此。
结合以上两种情况,我们知道mutable
可以用来修饰const对象中那些不会影响外部观察的成员。
volatile
volatile
的中文意思是易变的,但这个易变和mutable
是不同的含义。mutable
是指编译期的易变,根据语法编译器默认不会让我们修改某些变量,但是加上mutable
让编译器知道我们要修改的态度很强硬。而volatile
的易变是指运行期的易变,这些变化是编译器无法感知的变化,让编译器不要瞎优化。
volatile
如何影响编译结果
例一
编译器会将其认为无用的死代码优化掉。
int main()
{
int* reg = 0x123456; // 假设0x123456是某个特殊寄存器的地址
*reg = 0x1;
*reg = 0x2;
*reg = 0x3;
*reg = 0x4;
return 0;
}
上面的代码通过g++ a.cpp -O2
编译后,你会发现只有*reg = 0x4;
生效了,其他语句被优化掉了。
0x0000000000400540 <+0>: movl $0x4,0x123456
0x000000000040054b <+11>: xor %eax,%eax
0x000000000040054d <+13>: retq
一般情况下,这没有什么影响,但是上例中的代码实际是初始化某个设备的状态,任何状态的赋值都不能被省略,否则可能导致设备异常无法工作,此时就需要用到volatile
。当加上volatile
修饰后,同样通过g++ a.cpp -O2
编译后,没有任何语句被优化掉
0x0000000000400540 <+0>: movl $0x1,0x123456
0x000000000040054b <+11>: xor %eax,%eax
0x000000000040054d <+13>: movl $0x2,0x123456
0x0000000000400558 <+24>: movl $0x3,0x123456
0x0000000000400563 <+35>: movl $0x4,0x123456
0x000000000040056e <+46>: retq
例二
因为寄存器速度快,所以编译器会使用寄存器达到优化的目的,避免频繁操作内存。
玩过单片机或者ARM板的同学肯定都写过跑马灯程序,下面的程序就算简单模拟一下LED灯忽闪忽闪。
int main()
{
int* a = (int*)0x123456;
for( int i = 0; i < 1000; ++i ){
*a = ~(*a); // 反复取反,开关LED灯
sleep( 1 );
}
return 0;
}
通过g++ a.cpp -O2
编译后,你会发现每次取反后的值都保存在寄存器edx
中,只在最后把edx
里的值保存到了0x123456,这相当于只开关了LED灯一次,不符合预期。
0x0000000000400540 <+0>: mov 0x123456,%edx
0x0000000000400547 <+7>: mov $0x3e8,%eax
0x000000000040054c <+12>: nopl 0x0(%rax)
0x0000000000400550 <+16>: sub $0x1,%eax
0x0000000000400553 <+19>: not %edx
0x0000000000400555 <+21>: jne 0x400550 <main+16>
0x0000000000400557 <+23>: mov %edx,0x123456
0x000000000040055e <+30>: xor %eax,%eax
0x0000000000400560 <+32>: retq
当加上volatile
后,每次取反结果都会存入0x123456,开关LED灯1000次符合预期。
=> 0x0000000000400540 <+0>: mov $0x3e8,%edx
0x0000000000400545 <+5>: nopl (%rax)
0x0000000000400548 <+8>: mov 0x123456,%eax
0x000000000040054f <+15>: sub $0x1,%edx
0x0000000000400552 <+18>: not %eax
0x0000000000400554 <+20>: mov %eax,0x123456 # 每次取反结果都会存入0x123456
0x000000000040055b <+27>: jne 0x400548 <main+8>
0x000000000040055d <+29>: xor %eax,%eax
0x000000000040055f <+31>: retq
上面这个例子还可以反一下,即程序统计外部开关LED灯的开关次数(通过中断感知),如果使用寄存器优化的话,CPU可能只感知到一次变化。
例三
编译器可以重排指令方便指令流水化处理,达到优化目的。
int main()
{
int* a = (int*)0x123456; // 0x123456是某个设备地址
*a = 0x1; // 开启该设备
int* b = (int*)0x654321; // 0x654321是该设备的数据读取地址
printf( "%d\n", *b ); // 读取该地址的数据
return 0;;
}
上面代码的逻辑是开启某个设备后,从该设备指定地址读取数据,存在因果关系,但编译器无法识别。因此通过g++ a.cpp -O2
编译后,你会发现*b
被提前了,程序不符合预期。
=> 0x0000000000400590 <+0>: sub $0x8,%rsp
0x0000000000400594 <+4>: mov 0x654321,%esi # 先从0x654321读取数据放入esi作为printf的第二个参数,也就是先执行了*b
0x000000000040059b <+11>: movl $0x1,0x123456 # 后将0x1设置到0x123456
0x00000000004005a6 <+22>: mov $0x400760,%edi # "%d\n"放入edi作为printf的第一个参数
0x00000000004005ab <+27>: xor %eax,%eax
0x00000000004005ad <+29>: callq 0x400530 <printf@plt>
0x00000000004005b2 <+34>: xor %eax,%eax
0x00000000004005b4 <+36>: add $0x8,%rsp
0x00000000004005b8 <+40>: retq
使用volatile
修饰a和b后,指令执行顺序符合预期
=> 0x0000000000400590 <+0>: sub $0x8,%rsp
0x0000000000400594 <+4>: movl $0x1,0x123456 # 先将0x1设置到0x123456
0x000000000040059f <+15>: mov 0x654321,%esi # 后从0x654321读取数据放入esi作为printf的第二个参数,也就是先执行了*b
0x00000000004005a6 <+22>: mov $0x400760,%edi
0x00000000004005ab <+27>: xor %eax,%eax
0x00000000004005ad <+29>: callq 0x400530 <printf@plt>
0x00000000004005b2 <+34>: xor %eax,%eax
0x00000000004005b4 <+36>: add $0x8,%rsp
0x00000000004005b8 <+40>: retq
volatile
保证了单一执行线程内,对其修饰的变量的访问不能被优化,以及对另一先序或后序该volatile
变量的volatile
变量的访问不会被重排序。但需要注意volatile
并不能保证多线程安全。
上述代码运行必崩溃,看看意思就好。
gcc version 4.8.5 20150623