【翻译】C++编程:传值还是传const&
原文:https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce译者:0xAA55
# C++编程:传值还是传const&
以下的代码是错的。试着找出里面的错误吧。
void foo(const std::shared_ptr<Widget>& widget);
## 经验法则
作为C++程序员,你大概记得在刚跳进这粪坑的时候,问过的这样的问题:
> 你:请问老师,`string`周围的这些是啥?为什么`int`可以直接是`int`,但`string`却必须是`const string&`?
>
> 老师:吧啦吧啦吧啦,(在黑板上写写画画),吧啦吧啦吧啦。
>
> 你:哦我懂了(你当然懂了),老师,那既然这样的话我什么时候应该用哪个?
>
> 老师:这好办。所有这些内置的类型比如`int`、`bool`、`char`应该直接传值。但其它类型比如结构体或者类,就需要用`const xxx&`的方式传递引用。哦对了,`string`它并不是一个内置类型,它是个类,而且你看,你的 Visual Studio 都把它显示成了别的颜色。
或者你并没有老师,你有的是个先辈,或者你干脆就是自己百度谷歌然后抄 stackoverflow 的那种类型的家伙。不论如何,你选择了一个非常简单的规则,这作为初学者而言,“大道从简”是一件好事,因为它可以让你知道如何快速上手,但有时候也会让你被问题卡住。
但这还没完,你会在学习中再次回到这个问题上来。因为你有机会读到Scott Meyers写的《高效开发C++:55种方式改善你的程序和设计》(Effective C++: 55 Specific Ways to Improve Your Programs and Designs)。其中就有:“第20则:传参应该多传引用少传值。”这里说的正好就是这个问题。你一边读一边发出“我懂了”的声音,一边看到这样的两行总结:
> 请记住:
>
> * 应该尽可能传引用而不是传值。它更加高效而且避免你的程序代码运行起来上下不搭边。
> * 但是这条法则并不适用于内置类型、STL遍历器、函数对象。对于这些东西,通常情况下传值更合理。
我打赌你读到“STL遍历器”之前就跳过了内容直接看下一条了。
并且我相信你发现了我们实际上讨论的是对于智能指针`std::shared_ptr`应当传值还是传`const`引用的问题。
## 对于机器而言是原型,对于你而言是接口
函数原型定义了函数的返回值和参数的个数与类型。你在看“声明”的时候看到的基本就是这些玩意儿。
std::istream& getline(std::istream& input_stream, std::string& str);
这个是简化了的众所周知的`std::getline`函数。这样的声明包含了对于编译器而言必要的信息。编译器需要知道输入参数的数量,它们的大小和顺序,从而生成机器指令来完成调用。与此同时,它还包含了对于人类,也就是这个函数的使用者而言的有价值的信息。人类靠直觉判断出`str`是一个输出参数,用来接收函数的返回结果,也就是这个函数读出来的“行”。当然如果它是`const std::string& str`,那就完全不是这回事儿了。但实际上加上去的`const`修饰并不会让编译器产生的机器码和不加`const`修饰的时候那个函数相关的机器码有什么不同。事实上,你的编译器在生成机器码的时候并不管函数参数有没有`const`修饰。“Const正确性”对于C++而言是一个很美好的概念。它定义接口的人和使用接口的人之间的契约。
总之就是你定义你的接口,让你的用户知道你的接口是干嘛的,你是怎么设计它的。
## 回到代码
当我们说代码是错的,并不是说代码有Bug。要么代码性能不行,要么代码“有方言”。对于“代码方言”这一点,它在技术上是好东西。它让你知道虽然代码能运行(因此不是Bug),但是会警告你代码有深层次的问题。这些“代码方言”的说法因语言而异,因行业而异。
为了理解代码错在了哪,让我们先快速回顾一下`std::shared_ptr`是什么。它是一个智能指针,用 _引用计数_ 来包装一个指针,并且提供了像指针的接口。在任何时候,引用计数都可以指示出有多少`shared_ptr`在 **共同拥有** 这个资源。在每次增加一个拥有者的时候,引用计数的数值增加,而在拥有者被销毁(生命周期外,或者被析构)的时候引用计数会减少。最终,在引用计数减到零的时候,原始指针被delete,资源被释放。
在考虑到这一点的情况下,我们再看一遍函数,响亮地想出来!
void foo(... widget);
我们知道类型里肯定某处会写个`Widget`,但它具体是如何的呢?其实是要看情况的。我会问我自己以下三个问题:
* 我是否需要在函数里修改原对象?
* 复制一个Widget是否昂贵?
* 我是否需要负责管理对象的生命周期?
这些问题有8(2³)个可能的答案组合,但是由于它们之间互相关联,因此其中一些组合是无关的。
| Q1| Q2| Q3| 参数 |
|-----|-----|-----|------------------------------------|
| Yes |-| Yes | std::shared_ptr<Widget> widget |
| Yes |-| No| Widget& widget |
| No| Yes | Yes |std::shared_ptr<const Widget> widget|
| No| Yes | No| const Widget& widget |
| No| No|-| Widget widget |
在继续之前,我建议做个实验:从右往左反过来看这张表,先看参数,再尝试回答上面列出的问题。想象一下,你阅读的代码里正好有这么一个函数,它使用了这样的参数,思考一下这样使用参数的意图。
因此,不仅特定的情况决定了接口的设计,而且接口的设计也解释了特定的情况。考虑到这一点,让我们最终回到我们从开头的时候讨论的代码。
为什么我们的表种没有`const std::shared_ptr<Widget>&`的情况?是我们忘了试验它嘛?让我们通过反向思维的方式来找出答案:如果有人设计了这样的接口,他的理由是什么。
void foo(const std::shared_ptr<Widget>& widget);
我会如此解读:
* 我需要一个指向`Widget`的指针,但不是一般的指针,我要一个智能的,具体来说就是共享的指针。
* 但它并不是一个`shared_ptr<const Widget>`,因为我会修改原对象,所以请注意。
* 还记得我说过要共享指针吗?我其实并不想复制一个共享指针并成为一个共有者。基本上,我并不会去使用它的共享的部分相关的功能,但如果这个对象没有被封装进共享指针,则它应该被封装进共享指针。如果是这样,通常就会造成 **[双重释放异常](https://godbolt.org/)** 。
这不仅没有道理,而且它的方言听起来还像个合法代码。它带来了不确定性和潜在的新错误。
## 结论
当你经常编写C++,你就会经常看到const&,以至于你习惯了它、不去质疑它的正确性。绝大多数情况下使用const&是对的。让shared_ptr如此特别的是其独特的理念。但这并不是C++语言和STL中唯一的理念。对它们应当采取新的观点,比如去思考`std::remove`的算法为何要erase配套((https://zh.wikipedia.org/wiki/Erase%E2%80%93remove%E6%83%AF%E7%94%A8%E6%B3%95))
多观察“第20则:传参应该多传引用少传值。”提到的“这条法则并不适用于内置类型、STL遍历器、函数对象”。其实可能有更多的地方不应该适用这条规则呢。总之,多读书可以显著改变生活。
马丁·福勒(Martin Fowler)曾经有句著名的话:“_任何傻瓜都可以编写计算机可以理解的代码。然而只有好的程序员才能编写人类可以理解的代码。_”代码除了指示机器如何工作以外,它还有除此以外的职能。我们需要设计出能展现用途、理念的,清晰而又富有表现力的界面。对于正确合理的计算机开发非常重要。
watermelon 发表于 2021-5-10 08:51
Fortran里面为了保证效率与内存空间,只能传地址(笑哭)
C++是传错了就会造成性能浪费和逻辑错误,并不是单纯的规则。 通常来说,需要修改变量的值就【传引用】,否则【传值】。 美俪女神 发表于 2021-5-5 16:32
通常来说,需要修改变量的值就【传引用】,否则【传值】。
大错特错。请阅读原文。 Fortran里面为了保证效率与内存空间,只能传地址(笑哭) 0xAA55 发表于 2021-5-10 18:03
C++是传错了就会造成性能浪费和逻辑错误,并不是单纯的规则。
的确是这样的,可以可以。
页:
[1]