读c++primer有感1---const

来源:互联网 发布:九宫图算法双色球 编辑:程序博客网 时间:2024/05/17 07:34

前言:

c++关键字const,从字面意思上来说期望被const修饰的变量、对象、引用、类成员函数等拥有某种“保持不变”的特征、然而这些特征很多可以被破坏掉,甚至c++自身就提供破坏这些特征的手段。可以说const更类似于一种君子协定,比如函数设计者希望用户认为可以放心地使用函数,此时const X &obj保证不会让用户感知到obj前后变化;类设计者设计const成员函数告诉编译器可以让const对象安全地调用函数;编译器可能会根据const提示进行优化如常量替换(当然优化级别够高不const提示也可能会)。


一、c++ primer原话:如果对象是一个常量,使用const_cast执行写操作就会产生未定义的结果。例1:

#include<cstdio>
#include<iostream>

int main(){
    const int i = 10,*pi = &i;
    int j,k;
    int &ri = const_cast<int &>(i);
    ri = 20;
    j = i;
    k = ri;
    std::cout<<i<<std::endl;
    std::cout<<j<<std::endl;
    std::cout<<*pi<<std::endl;
    std::cout<<k<<std::endl;
    std::cout<<ri<<std::endl;

    return 0;
}
直接g++ test.cpp不加优化。
输出

10
10
20
20
20
看输出后的汇编代码(g++ -S)和实际输出结果可以得知:

1.有i的地方都被替换成了立即数10;

2.i的对应栈数值的确修改了。

结论:感觉编译器傲娇的说“你告诉人家是个const,那人家当然认为你不会变了”的感觉。这种感觉在类函数没有被声明virtual,但是子类却也实现同名函数时,父类指针或引用不会表现多态性也有。不是virtual,说明父类本来就不打算让子类重新实现。

例2:

#include<cstdio>
#include<iostream>
namespace{
    const int gi = 100;
}

int main(){
    int &ri = const_cast<int &>(gi);
    ri = 200;
    std::cout<<gi<<std::endl;
    std::cout<<ri<<std::endl;

    return 0;
}
结果是sigseg,段错误了。gdb看core发生在ri=200那一行。

还是看汇编代码发现ri被放到了rodata段,作为只读数据,ro--readonly。

如果觉得看汇编麻烦,也可以nm a.out |grep -i 'gi'来看,会得到‘080487e0 r _ZN12_GLOBAL__N_1L2giE’,可以看到name mangling后的gi是个只读的(r)。

结论:感觉编译器更傲娇了“认为你不会变心,就告诉操作系统哥哥把你的段属性设成了不能改变了”。


二、const对象只是宣称是const,真想变const编译器不仅不管,还会帮你

1.编译器明确的辅助手段

const_cast,上面例子已经体现。

mutable成员,例子:

#include<cstdio>
#include<iostream>
class MyInt{
    public:

        mutable int i = 1000;
        int j = 1000;
        const int k = 1000;

};

void changeConst(const MyInt &temp,int count){
    if(count == 0){
        return;
    }else if(count == -1){
        temp.i = 50;
        MyInt &temp2 = const_cast<MyInt &>(temp);
        temp2.j = 50;
    }
    changeConst(temp,count - 1); //弄成递归时优化-O时不会跟进递归函数,-O3时会
}

int main(){
    const MyInt cmi;
    changeConst(cmi,20);
    if(100 < cmi.i) //改成cmi.j效果一样,cmi.k也是
        std::cout<<"yes"<<std::endl;

    return 0;
}
这里有几个现象:

cmi尽管是const的,尽管cmi.k看上去也是const的(大笑),但是100<cmi.k没有常量替换;

-O优化时,汇编代码反应都会进入changeConst递归五十次之后,按照i或者j实际的值输出“yes”;

-O3优化,汇编代码反应都不会进入changConst,并且也不会进入100 < cmi.i的条件语句,直接输出“yes”。

再翻书,得知c++11新支持的类成员初始化格式const int k = 1000其实是个默认值,为各种构造函数提供默认值,等效于初始化列k(1000),见例子:

#include<cstdio>
#include<iostream>
class MyInt{
    public:
        MyInt():i(2000){
            std::cout<<i<<" "<<j<<" "<<k<<std::endl;
        }
        mutable int i = 1000;
        int j = 1000;
        const int k = 1000;
};

int main(){
    MyInt mi;
}

2000 1000 1000,由此可见。

没有显式生成默认构造函数(就是不带参数这种),那么编译器会自动合成默认函数(不一定肯定会生成,例外见c++ primer)。按照c++ primer的说法,需要把构造函数显示声明为constexpr的,并且提供一个constexpr的k的getter函数,这样就能在编译时期确定k的值。

对于const和constexpr,按照stackoverflow上vote比较多的说法,const是对运行时的口头保证不变,constexpr是编译时期能确定的一种值用于编译时期使用。const是针对一块内存而言,大约指这块内存的值不会有变化;constexpr体现在代码,是否用立即数替换其他寻址。如下:

#include<cstdio>
#include<iostream>
class MyInt{
    public:
        constexpr MyInt():i(2000),k(2000){
        }
        inline constexpr int getK() const;
        mutable int i = 1000;
        int j = 1000;
        const int k = 1000;
};

constexpr int MyInt::getK() const{
    return k;
}

int main(){
    const MyInt cmi;
    if(100 < cmi.getK())
        std::cout<<"yes"<<std::endl;

    return 0;
}

但是结果,cmi.getK()仍然被调用,常量表达式没有在编译时计算得出。甚至都没有被inline...

综上,const类成员k没有被常量替换正常,因为它是在构造函数内初始化的。但是按标准的constexpr来也不能编译时确定就见下回分解了。

第二点是int j和mutable int i两个成员,现象看来不同级别的编译器优化对二者处理相同。本想const MyInt的非mutable成员会被认为不变而被特别优化,起码看来包含4.8.2版本gcc的g++是这样。


2.猜const对象位置

#include<cstdio>
#include<iostream>

int main(){

    const int i = 100;
    int &ri = const_cast<int &>(i);
    int j = 200,*pj = &j;
    ri = 101;
    *(pj - 1) = 700;
    std::cout<<i<<std::endl;
    std::cout<<ri<<std::endl;

    return 0;

}

结果输出100,700,100是常量替换,700是根据另一个int j的位置猜测i在栈中位置改变的。栈内的能猜,堆中的连续分配的诸如类、结构体这种夹杂const和非const的估计也能改。像之前的例子一样,放在常量区的const不用担心,比如:

main.cpp:

#include<cstdio>
#include<iostream>
#include"test.h"
#include"test2.h"

int main(){
    innocentFun();
    std::cout<<gi<<std::endl;
    return 0;
}

test.h

#ifndef TEST_H
#define TEST_H
extern int gi;
#endif

test2.h

#ifndef TEST2_H
#define TEST2_H
#include "test.h"
void innocentFun();
#endif

test.cpp

#include"test.h"
const int gi = 50;

test2.cpp

#include "test2.h"
void innocentFun(){
    int &i = const_cast<int &>(gi);
    i = 5000;
}

g++ test.cpp test2.cpp main.cpp,执行会发生段错误。这样test2的开发者就没法用自己的纯洁无害的函数innocentFun恶意修改test的全局const变量了。

综上,const的破坏手段可以有编辑体提供的,以及也可以自己猜,因为const本身编译器只会对文本中的显示对const赋值进行干涉(mutable不管)。但是操作系统层次的段属性(看了文章说linux没有真正的段,但模拟的也是段)是没法变的,这种const从c++这个层次改不了。


三、成员函数的const

从某论坛c++版块的一个问题开始讲起。

#########################

能否判定对象是否是const?
  
void Object::function() {
     do something
     ....
     if (object is const) {
         xxx
     else {
         xxx
     }
}
  
Obj obj;
const Obj& const_obj = obj;
obj.function();
const_obj.function();
如何能实现那个if语句,使得两次调用function函数的时候走不同的分支?
本来可以实现 void function() 和void function()const;两个不同的函数,问题就解决了。
但是do_something那段代码特别长,copy两份代码不便于维护。

##########################

有一靠谱兄弟回答:

##########################

void object::function_impl(bool isconst);
void object::function()
{function(false);}
void object::function()const
{function(true);}

##########################

总结下:

1.const Obj obj要进入function,function必须是个const的。const成员函数可以看成是void Obj::function(const Obj * const this),非const的是个void Obj::function(Obj * const this),根据c++ primer中函数重载实参类型转化规则,const Obj obj的this指针const Obj * const this是不能够转化成后者的(底层const不能转化)。所以楼主的function必须是个const的,如果需要const Obj和Obj都能调用,当然用const和非const两个函数重载也可以;

2.如果只用一个const的function,c++ primer七百多页的内容也没讲从const成员函数内部能不能区分出当前对象的const属性。下面有一兄弟回了:

###########################

当然如果你一定要动态判断的话,还有std::is_const
我没有用过,我猜可以这么写:
if ( std::is_const<decltype(*this)>::value )

###########################

is_const看标准是类模板重载得来,下面给出一个类似的函数模板重载版本:

#include<cstdio>
#include<iostream>
class MyInt{
    public:
        int i = 1;
};

template<typename T>
bool is_const(T &obj){return false;}

template<typename T>
bool is_const(const T &obj){return true;}

int main(){
    MyInt cmi;
    if(is_const(cmi))
        std::cout<<"ture"<<std::endl;
    else
        std::cout<<"false"<<std::endl;
    return 0;
}

还有些可能的细节没关注,比如obj如果是右值或顶层指针啥的,但是这个简易版本能分出基本的const。

不过问题来了,如果是在const的function里面那么*this一定是const的...,因为已经经过实参向形参转化了。

#include<cstdio>
#include<iostream>
class MyInt{
    public:
        int i = 1;
        void function() const;
};

template<typename T>
bool is_const(T &obj){return false;}

template<typename T>
bool is_const(const T &obj){return true;}

void MyInt::function() const{
    if(is_const(*this))
        std::cout<<"true"<<std::endl;
    else
        std::cout<<"false"<<std::endl;
}

int main(){
    MyInt cmi;
    cmi.function();
    return 0;
}

再怎么都是true。

3.按照靠谱兄弟的回答。

void object::function_impl(bool isconst);
void object::function()
{function(false);}
void object::function()const
{function(true);}

能够根据const属性重载进不同的函数,const Obj 进function()const,Obj 进function(),但是调用实现函数function_impl也有坑。const 的function能调的只有const的function_impl,那么只能function_impl定义成const,在判断isconst的if语句中如果不是const,把this给const_cast成非const的。如:

#include<cstdio>
#include<iostream>
class MyInt{
    public:
        int i = 1;
        void notconst() const{MyInt *pmi = const_cast<MyInt *>(this);}; //尽管notconst要干很多不是const该干的事,但是为了能被const的function调用只能定义成const。
        void function() const{notconst();};
};

int main(){
    MyInt cmi;
    cmi.function();
    return 0;
}
实际上,c++ primer建议用const_cast的地方有点类似:

#include<cstdio>
#include<iostream>
class MyInt{
    public:
        int i = 1;
};

const MyInt &smallerMyInt(const MyInt &lhs,const MyInt &rhs){
    return (lhs.i < rhs.i)?lhs:rhs;
}

MyInt &smallerMyInt(MyInt &lhs,MyInt &rhs){
    const MyInt &rmi = smallerMyInt(const_cast<const MyInt &>(lhs),const_cast<const MyInt &>(rhs)); //必须const_cast回const不然重载不到const版本,就一直循环重载了。
    return const_cast<MyInt &>(rmi);
}

int main(){
    MyInt mi,mi2;
    mi2.i = 20;
    smallerMyInt(mi,mi2).i = 500; //为了返回左值能够赋值
    std::cout<<mi.i<<std::endl;
    return 0;
}

总结,const成员函数提示符是我个人认为c++面向对象编程最为强大的辅助工具,像前言说的一样,能给用户像接口文档一样给予提示,能够自己按照const与否重载方法,能让编译器对自己的不经意行为做出干涉。和前面两节的讨论const实现的某些较底层细节比起来,作为马农可以不用关心const到底优化了多少性能,但是基于工程目的应该做到能const则const。当然,这不是我这个菜鸟的乱侃,这是我看了七八篇stackoverflow之后,众老哥一致同意的结果。

1 0
原创粉丝点击