const与constexpr
c++开发中,常量属性是避免不了要接触的。如果运用不好,函数或变量的常量属性会给你造成麻烦。其中,把const和constexpr这两个关键字弄混是一大原因。(当然还有其他原因引起困惑。。)本文我们试图解决以下2个问题:
- const与constexpr的区别?
- 常函数的使用建议?
一、const与constexpr的区别
《c++ primer》中有对这个问题的详细介绍,但我一开始没怎么注意他嘛!那么我是怎么注意到这个问题的呢?实际开发中,经常会使用stl中的array容器来代替c风格静态数组:
int size=2;
array<int,size> arr;//第一次使用array容易或许你容易“天真”地写出这样的代码
然后你就会发现编译器报出这样的错误[GNU]:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ const_test.cc -o const_test && ./const_test
const_test.cc: In function ‘int main()’:
const_test.cc:15:15: error: the value of ‘size’ is not usable in a constant expression
如果看了上述代码你不知道咋回事,处于懵逼的状态,那么下面的代码更简单意识到问题:
int size=1;
const int c1=size;// OK
constexpr int c2=size;// the value of ‘size’ is not usable in a constant expression!
这么看是不是更容易搞明白二者的区别?
- constexpr修饰的变量同样具有**“常量”属性**,与const一样,不可修改。
- constexpr修饰的变量,只接受编译期就已经确定好的值或表达式,一般可以是立即数或者带有constexpr修饰的变量或函数,即,常量表达式。
constexpr int constFunc(){
int a=2;
return a;
}
int main(){
constexpr int c3=2;
constexpr int c4=c3;
constexpr int c5=constFunc();
}
上面的代码[GUN,c++14]可以看的很清楚了,constexpr除了必须接受一个编译期确定的量之外,有一个功能就是:创造出一个“编译期确定值”的语义。用constexpr修饰的函数或者变量都将被看作其值是编译器确定的!
但这里还有一个关于语言标准的小细节需要注意:在c++11中,constexpr修饰的函数体必须只有1行;而在c++14中则取消了这个限制,可以有任意行:
constexpr int constFunc(){
return 2;
}
constexpr int constFunc(){
int a=2;
return a;
}
二、常函数的使用建议
如果通读过《c++ primer》的话,会看到里面建议你:当你设计类时,应该尽可能地给一个不会修改成员变量的函数加上常函数修饰。
这个建议其实很对,但是我一开始并没有理解其中的必要性,因为我那时只考虑到常函数的性质之一:常函数里面只能调用常函数(成员函数)。所以我就觉得,即便不把它定义成常函数似乎也不会有什么影响啊,而且还对里面能调用的函数加以限制,这用起来岂不很麻烦:
class A{
public:
int data()const{return data_;}
int getData_1()const{
handleData();
return data_;
}
int getData_2()const{
doWork();
return data_;
}
void handleData(){}
void doWork()const{}
private:
int data_;
};
const修饰的函数,编译器将替你检查你是否有可能对成员变量做出修改,哪怕没有做出修改,但”有可能“修改,那也无法通过编译!比如上面的代码中,尽管handleData()函数中实际上并没有对data_做出修改,但只是因为没有const修饰,将会被认为是可能对成员变量做出修改的,无法通过编译!
基于此,可能会有初学者不是很喜欢为成员函数加上const,因为上述原因在代码量大的时候往往无法很快发现问题。。一度我也是这么想的,直到我拜读了侯捷老师的视频!侯捷老师向我们说明了一种情况,在这种情况下,就不得不尽可能地使用const了!
class MayConstObj{
public:
void handleData(){}
void doWork()const{}
private:
int data_;
};
int main(){
MayConstObj c1;
c1.handleData();
c1.doWork();
const MayConstObj c2=c1;
c2.handleData();
c2.doWork();
}
上述代码使用GNU,c++11测试过,报错信息为:
g++ -std=c++11 const_test.cc -o const_test && ./const_test
const_test.cc: In function ‘int main()’:
const_test.cc:21:19: error: passing ‘const MayConstObj’ as ‘this’ argument discards qualifiers [-fpermissive]
如果你对c++语言没有一个充分了解的话,相信看到这一串报错信息一定感到束手无策,然后就上网赶紧查查报错信息。。(曾经的我),但相信你用过c++一段时间后,可以轻易发现上面的语句为什么会导致这样的报错信息!
为了搞懂上面的报错信息,来举一个更直观的例子:
void func1(const int*){}
void func2(int*){}
int main(){
const int* a;
func1(a);
func2(a);
}
上述代码运行结果如下:
zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ -std=c++11 const_test.cc -o const_test && ./const_test
const_test.cc: In function ‘int main()’:
const_test.cc:29:11: error: invalid conversion from ‘const int*’ to ‘int*’ [-fpermissive]
这样看,原因就很显然了:常量指针(指向常量的指针)无法隐式转换为正常指针(所以才需要const_cast嘛!但要注意的是,常量却可以隐式转换成非常量、指针常量也可以隐式转换成正常指针)。
而非静态类成员函数的第一个参数[GNU实现]是一个this指针,非静态非常量类成员函数的第一个参数是一个const T*(常量指针)!也就是说,常量对象c2的handleData()类成员函数,编译期为其生成的函数原型应该是:
MayConstObj::handleData(MayConstObj*)
而doWork()类成员函数,编译期为其生成的函数原型是:
MayConstObj::handleData(const MayConstObj*)
所以,当类的使用者以常量形式调用非常类成员函数时,常量形式对象的this指针也是一个常量指针,就意味着传入成员函数的this指针都是const T*,相当于:
const MayConstObj c2=c1;
const MayConstObj* this=&c2;
MayConstObj::handleData(this);
如此,就产生了上面的报错信息。明白了上述道理后,修改这个问题并不需要上网查查,先查看一下常量对象调用的成员函数是不是常函数就行了!
最后,总结一下常函数的作用和建议:
- 常函数只能调用常成员函数,并且不能修改成员变量,否则将无法通过编译。
- 常量对象只能调用常成员函数,调用非常成员函数将导致指针类型转换拒绝。
- 基于以上两点,设计一个类时,尽量将不会改变成员变量的函数设置为常函数,除非你咬定你的类将来绝不可能以常量实例化,或者保证常量实例化绝不可能调用该成员函数。
最后,我们加点料!在c++中,我们有时需要确切的知道一个函数的原型,这个小技巧在这时会很实用,大概有2种方法,下面的命令均在GNU下测试过:
-
输出目标文件符号表+解析符号: 比如上面的类成员函数MayConstObj::handleData() : zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ -std=c++11 const_test.cc -o const_test
zkcc@LAPTOP-OHBI7I8S:~/mytest$ readelf const_test -a |grep MayConstObj
62: 0000000000001288 15 FUNC WEAK DEFAULT 16 _ZN11MayConstObj10handleD
66: 0000000000001298 15 FUNC WEAK DEFAULT 16 _ZNK11MayConstObj6doWorkE
zkcc@LAPTOP-OHBI7I8S:~/mytest$ nm const_test |grep MayConstObj
0000000000001288 W _ZN11MayConstObj10handleDataEv
0000000000001298 W _ZNK11MayConstObj6doWorkEv
zkcc@LAPTOP-OHBI7I8S:~/mytest$ c++filt _ZN11MayConstObj10handleDataEv
MayConstObj::handleData()
你会发现,这个类成员函数的解析,缺少了默认的this指针参数啊(这个函数也确实不是静态啊!)。。 确实,这就需要用到第二种方法了。 -
利用gdb的栈轨迹来帮我们查看函数原型,可以解析出类成员函数的this指针实现: zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ -g std=c++11 const_test.cc -o const_test
zkcc@LAPTOP-OHBI7I8S:~/mytest$ gdb const_test
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Reading symbols from const_test...
(gdb) b const_test.cc:11
Breakpoint 1 at 0x1298: file const_test.cc, line 11.
(gdb) r
Starting program: /home/zkcc/mytest/const_test
Breakpoint 1, MayConstObj::doWork (this=0x7fffffffdd54) at const_test.cc:11
warning: Source file is more recent than executable.
11 int a=0;
(gdb) bt
(gdb)
|