Re: [问题] 暂时物件产生的原因

楼主: sarafciel (Cattuz)   2022-01-11 07:13:37
※ 引述《WangDaMing (王大明)》之铭言:
: 开发平台(Platform): (Ex: Win10, Linux, ...)
: Linux
: 编译器(Ex: GCC, clang, VC++...)+目标环境(跟开发平台不同的话需列出)
: GCC
: 最近看到一个例子不太懂这是c++的什么机制让他产生暂时物件的
: #include <iostream>
: #include <string>
: using namespace std;
: int main(){
: pair<const string,int> data = {"123",5};
: const pair<string,int> &ref = data;
: }
: 我看文章说因为data的first是const可是ref的first没有const但是编译器
: 不会让他编译错误会产生暂时物件.
: 1.可是这边我就不懂了,是什么机制让他产生暂时物件的?有这机制的名称吗??
: 还有为何不让他编译错误要帮他产生暂时物件??
: 2.这种暂时物件新手蛮容易犯错的,
: 有比较好的方式可以帮助我们确认是否产生暂时物件吗??
: 我知道书上推荐用auto不过如果先不考虑auto有什么方法确认吗??
: 感谢各位
: ※ 编辑: WangDaMing (111.248.244.154 台湾), 01/10/2022 22:18:42
Well,我不知道你看的是哪篇文章
不过"编译器不会让他编译错误"是一个不算错但容易误导的讲法XD
这段程式码好玩的地方在于,你把下面那行的const拿掉,他就编不过了
但是如果你把const跟reference都拿掉,突然又可以编过了:
https://godbolt.org/z/en8rWP511
所以到底是编的过还是编不过?碰到这种情况该怎么判断?
实际上你应该从最简单的,也就是没有const,也没有reference的情况开始验证:
pair<string,int> ref = data;
这样一段程式码编译器判断是OK的,代表有两种可能性:
(1).pair<const string,int>在编译器的认知中,跟pair<string,int>为同型态
(2).pair<const string,int>跟pair<string,int>不同型态,
但pair有提供转换的constructor,然后编译器帮你做implicit conversion
如果加了reference就编不过去的话,实际上(2).是比较有可能的
因为reference基本上是要求跟指到的对象type要完全一致(或者是子类)
但这个case是template的parameter多带了一个const,
到底会不会被判定成不一样可能会让对template不熟的人,比较疑惑一点
所以这边就需要写code做一点验证,假设(2).是对的
那理论上我们写一个很类似pair的template出来,
但不提供转换的constructor,这时候编译器就会报错:
https://godbolt.org/z/8f95rrTdG
如果你对type_traits有一点认识,其实也可以用is_same去做检验:
https://godbolt.org/z/a8ahYnT49
不论是哪一个都可以看出来,这两个型态应该是认定为不一样的
所以之所以用std::pair可以过,不是什么"编译器不会让他编译错误"
而是std::pair主动去给出constructor,把有cv qualifier的情况再涵盖近来
这个std::pair的constructor写法类似下面这样
不过为了阅读方便起见没写到很严谨,参考就好:
https://godbolt.org/z/GaEsMWqqf
了解了这个基本事实后,我们再把const跟reference加回去:
1.pair<string,int> & ref = data;
不能过,因为reference要求相当严格的型态一致
2.const pair<string,int> & ref = data;
可以过,但是为什么可以过?
这是这段程式码第二个tricky的地方,因为const reference允许隐式转换与右值
关于const reference接了右值之后会做什么事,Sutter大神有写过文章可以参考:
https://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/
版上应该也有讲解过这个性质的文章,可以往前翻挖一挖宝藏。
正常来说右值的life cycle是定义到使用他的expression结束时消灭(呼叫destructor)
但透过const reference的绑定,可以将右值的生命周期延续到reference消灭为止
在C++11之前因为没有rvalue reference,
有时候会用const reference这个性质让右值留久一点做一些方便的应用。
C++11以后一般就是用rvalue reference做这件事了
不过为了相容性,const reference还是保留了接右值的能力
const reference由于有const的承诺
本身在型态上的要求并没有non-const reference如此严格
non-const reference之所以要有严格的型态限制
其中的一个原因就是你有可能会用这个换过型态的reference去改写原变量的值
但const reference限定只读,这件事反而不会发生
所以const reference允许你去做隐式转换:
int x = 3;
const long & y = x; //implicit conversion from int to long
但是const reference实际上不是某个真正的instance,他只是个包过的指标而已
所以这里就会有一个问题产生,也就是这个指标到底该指到什么东西?
应该指到x吗?但指到x是很危险的一件事,以x64的情况来说
因为x只有4个byte,long则有8个byte,就算y的功能只是read value
你也很有可能因为超界存取导致y读到一个很奇怪的值,
所以这里他就必须要生一个真正的long出来:
int x = 3;
long temp = long(x);//你没有写,但compiler会帮你补
const long &y = temp;
这是为什么在隐式转换给const reference之后,会有一个"暂存值"的原因
如果上面这一大坨有看懂的话,要回答你的问题就很简单了:
1.之所以会有暂存物件,是因为你用了const reference
但就算是const reference,没有办法隐式转换的型态编译器也是会挡的
这个例子会成功是因为,
std::pair有提供在模板参数多cv qualifier的场合也能建构的constructor
导致assign给const reference时触发隐式转换
(确切的说,std::pair的constructor定的抽象非常强,
不单是加减cv qualifier的场合,只要可以被转换成该型态就可以丢进constructor
原PO的第一行其实就已经在用这个强抽象的好处了)
2.这个例子的触发条件有三个:
(a).const reference
(b).可隐式转换的type跟constructor
(c).assign给const reference的expression其type必须触发隐式转换
这里面最不可能发生的事情应该是(c),
因为99%的情况你会给const reference的值应该是要跟const reference型态一致的
所以(c)发生时比较大的可能应该是写错型态了
如果是要避免这个问题的话,可以用type alias去弄一个简单好记的alias来用:
https://godbolt.org/z/KcW9YEcbo
或是用std::reference_wrapper跟std::cref去避免隐式转换发生:
https://godbolt.org/z/vrb5TbefW
大概是这样,有错还请版友不吝指正。
作者: g0010726 (Kevin)   2022-01-11 08:56:00
https://en.cppreference.com/w/cpp/utility/pair/pair其实看一下reference 就知道了 第(4)个就是了
楼主: sarafciel (Cattuz)   2022-01-11 10:48:00
YA cppref有写 直接贴这篇我可以省一半篇幅XD

Links booklink

Contact Us: admin [ a t ] ucptt.com