出参数并通过参考

我join了一个新的小组,编码准则(对我来说)似乎过时了。

但是只有在没有有效备份的情况下才能对抗机器,不会让我感觉到任何地方。
所以我现在转向SO,看看我们是否能够理性地推翻/反对(嘿,我可能在我的select上是错误的,所以争论的双方都会被赞赏)。

争论的准则是:

提示:使用指针而不是引用来返回参数。

void Func1( CFoo &Return ); // bad void Func2( CFoo *pReturn ); // good 

理由:
在使用引用时,它看起来与值相同。 调用者可能会惊讶于在调用该函数之后其值已被更改。 被调用者可能会无意中修改该值,而不会影响调用者的价值。 通过使用指针,调用者和被调用者都清楚该值是可以改变的。 在代码审查中使用引用可能会造成误导。

在使用引用时,它看起来与值相同。

只有当你真的没有注意到你在做什么。 好的,有时会发生,但真的…没有多less编码标准可以纠正不注意或不知道自己在做什么的人。

调用者可能会惊讶于在调用该函数之后其值已被更改。

如果您对调用某个函数时发生的情况感到惊讶,则该函数的logging不完整。

给定一个函数的名字,参数列表,或许是一些非常简短的描述性文档,应该明确地说明函数做了什么以及它可观察到的副作用(包括是否修改了参数)。

被调用者可能会无意中修改该值,而不会影响调用者的价值。

如果函数是const正确的,那么这不是问题。 如果函数不是const正确的,那么它应该是正确的,如果可以的话(追溯性地使代码const正确可以是绝对的跳动)。

然而,这个基本原理没有多大意义:当你正在编写一个函数的代码时,你应该能够看到参数的声明。 如果函数太长,你不能,那么重构就是时候了。

通过使用指针,调用者和被调用者都清楚该值是可以改变的。

这不完全正确。 一个函数可以接受一个指向const对象的指针,在这种情况下,对象是不能改变的。

在代码审查中使用引用可能会造成误导。

只有做代码审查的人不知道自己在做什么。


所有这一切都很好,但为什么应该使用引用而不是传递指针呢? 最明显的原因是参考不能为空

在一个需要指针的函数中,在使用指针之前,必须检查指针是否为空,至less在debugging断言的时候。 在正确的代码审查期间,您必须分析更多的代码,以确保您不会意外地将一个空指针传递给一个不期望的函数。 我发现,由于这个原因,复习带指针参数的函数需要更长的时间。 当使用指针时,它会变得很容易。

在我看来,正确使用const会(大部分)消除这个提示的需要。 看起来有用的部分是在读取来电显示时,看到:

 Func1(x); 

不清楚x是怎么做的(特别是像Func1这样的不起眼的名字)。 而是使用:

 Func2(&x); 

用上面的约定,向调用者指出他们应该期望x被修改。

虽然我自己不会使用提示的build议,但理由是有效的,这就是为什么像C#这样的语言在调用站点中引入outref关键字的原因。

我可以提出的最好的论点是:不要求人们使用指针,而是要求人们编写反映函数function的函数名称 。 当我调用std::swap ,我知道它会改变参数的值,因为这个名字暗示了这一点。 另一方面,如果我要调用函数getSize ,我不希望修改任何参数。

如果您尚未购买Herb Sutter和Andrei Alexandrescu的“C ++编码标准:101规则,指南和最佳实践”。 阅读。 推荐给你的同事。 这是本地编码风格的良好基础。

在规则25中,作者build议:

“如果参数是必需的,并且函数不会存储指向它的指针或者以其他方式影响它的所有权,那么更喜欢通过引用来传递参数,这表明参数是必需的,并且使得调用者负责提供有效的对象。

“需要参数”表示NULL不是有效的值。

导致缺陷的最常见原因之一是空指针的意外解除引用。 在这些情况下使用引用而不是指针可以在编译时消除这些。

所以你有一个权衡 – 消除错误的频繁来源,或确保通过除函数名称以外的方式调用代码的可理解性。 我个人倾向于消除风险。

如果他们真的想在调用网站上明确提到out参数,他们实际上应该要求,而不是通过尝试做出指针意味着什么,而不是黑客。 指针并不意味着修改比引用更多,并且为非修改对象传递指针并不罕见。

明确expression参数的一种可能方式是:

 template<class T> struct Out { explicit Out(T& obj) : base(obj) {} T& operator*() { return base; } T* operator->() { return &base; } private: T& base; }; template<class T> Out<T> out(T& obj) { return Out<T>(obj); } void f(Out<int> n) { ++*n; } int main() { int n = 3; f(out(n)); cout << n << '\n'; } 

作为一个临时措施,直到他们将旧代码更改为此,您可以将Out转换为指针和/或引用:

 // in class definition operator T*() { return &base; } operator T&() { return base; } // elsewhere void old(int *p); void g() { int n; old(out(n)); } 

我继续写下这个所需的各种类,并且以一种应该很好地降解的方式写入input参数。 我怀疑我很快就会使用这个惯例 (至less在C ++中),但是对于任何想要使得调用站点明确的人来说,这是有效的。

编码标准和习惯一样基于常识。 你的一些同事可能会依赖多年根深蒂固的假设,即不通过指针传递的参数不会改变 – 可怜他们。

编码标准的重要部分不是它们是最优的,而是每个人都遵守这些标准,以便代码体有一定的一致性。

我发现这里有两所学校:

  • (a)使用指针显示参数可能被修改
  • (b)当且仅当参数可以为空时使用指针。

我同意你(a)的动机:在阅读代码时,即使鼠标hover给你声明函数,你也不能知道所有的声明。 在成千上万条线路中放置数百个function只需要时间。

如果你混合input和输出参数,我当然会在这里看到一个问题:

 bool GetNext(int index, Type & result); 

这个function的调用看起来像这样:

 int index = 3; Type t; if (!GetNext(index, t)) throw "Damn!"; 

在这个例子中,调用本身是相当明显的,可能会修改t 。 但是index呢? 也许GetNext递增索引,所以你总是得到下一个项目,没有被调用者需要保持调用者状态?

其中通常引发的答复然后该方法应该是GetNextAndIncrementIndex ,或者你应该使用迭代器 。 我敢打赌,这些人从来不必debugging由电气工程师编写的代码,仍然认为数字食谱是编程的圣杯。

无论我还是倾向于(b):仅仅是因为可以避免新代码被写入的问题,并且“可能无效”通常是更常见的问题。

理由是逻辑正确的。
这可能会让编码人员感到价值已经发生了变化(因为他们认为价值正在被价值传递)。

但在逻辑上真正在这方面提供任何意义。
所以价值可能会改变。 这是如何影响代码的正确性的?
除了它可能会打印出不同的值,然后是不合逻辑的人的期望,但代码正在做它应该做的事情,编译器正在执行约束。

我build议:

  • 通过引用传递(不要通过指针传递)
  • 尽可能地传递const引用(假设你已经在你的代码库中正确地使用了const)
  • 放置在列表开头突变的参数/参数
  • 恰当地标记function
  • 恰当地标出论点(并用详细的描述性名称和less量论据来创build方法/函数)
  • logging结果
  • 如果多个参数/参数变异,请考虑创build一个简单的类来保存这些参数(即使通过引用自身)
  • 如果它们仍然不能在没有可视化和logging的提示的情况下运行(原文如此),则为参数进行变异创build一个轻量级模板容器对象,然后将其传递给方法或函数

我不同意这个指导原则。 理由中提到的混淆可以通过确保代码是const正确的来轻松解决。 如果通过引用将inputparameter passing给一个函数,那么它应该是一个const引用。 如果引用不是const ,则表示它是一个输出参数,其值可能会被函数改变。

此外,当你传递一个函数指针而不是引用时,会立即引发一个关于这是否是指向dynamic分配内存的指针,以及是否应该释放的问题。 使用引用删除了调用delete的诱惑。

有时候传递一个指针是合适的,例如当它实际上是一个指向dynamic分配的对象或数组的指针时,或者当它有意义的时候它是空的。 虽然,在这种情况下,你应该更喜欢智能指针。 在所有其他情况下,参考是更好的,恕我直言。