输出参数并通过引用传递

Mar*_*ork 39 c++ pointers coding-style reference

我加入了一个新的小组,其编码指南(对我而言)似乎过时了.

但只是在没有有效备份的情况下反对机器不会让我无处可去.
所以我转向SO,看看我们是否能够理性的理由支持/反对(嘿,我的选择可能是错的,所以我们会赞赏论证的双方).

争论的准则是:

提示:对返回参数使用指针而不是引用.

void Func1( CFoo &Return );  // bad  
void Func2( CFoo *pReturn ); // good  
Run Code Online (Sandbox Code Playgroud)

理由:
使用引用时,它看起来与值相同.调用该函数后,调用者可能会惊讶于他的值已被更改.被调用者可能会无意中修改该值而不会影响调用者的值.通过使用指针,调用者和被调用者都清楚可以更改该值.在代码审查中使用引用可能特别容易引起误解.

Jam*_*lis 35

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

只有你真的没有注意到你在做什么.好的,有时会发生这种情况,但实际上......没有多少编码标准可以纠正那些不注意或不知道自己在做什么的人.

调用该函数后,调用者可能会惊讶于他的值已被更改.

如果您对调用函数时发生的情况感到惊讶,那么该函数的记录很少.

给定一个函数的名称,它的参数列表,以及一些非常简短和描述性的文档,它应该非常清楚函数的作用以及它的可观察副作用是什么(包括是否修改了任何参数).

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

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

但是,这个基本原理没有多大意义:当您实际编写函数的代码时,您应该能够看到参数的声明.如果函数太长而你不能,则需要重构.

通过使用指针,调用者和被调用者都清楚可以更改该值.

这不完全正确.函数可以获取指向const对象的指针,在这种情况下,无法更改对象.

在代码审查中使用引用可能特别容易引起误解.

只有进行代码审查的人不知道他们在做什么.


所有这一切都很好,但为什么应该使用pass-by-reference而不是pass-by-pointer?最明显的原因是引用不能为空.

在一个带指针的函数中,你必须在使用之前检查指针是否为空,至少在调试断言时是这样.在正确的代码审查期间,您必须分析更多代码,以确保不会意外地将空指针传递给不期望的函数.我发现由于这个原因,审查带有指针参数的函数需要更长的时间; 使用指针时更容易弄错.

  • @Mark Ransom:那不是真的.在取消引用它们之前,需要检查指针.但是一旦你有一个有效的参考,它就不会变得无效.因此,如果您始终使用引用(而不是指针,则可以进入该情况). (6认同)
  • @Martin,理论上你是对的.但实际上你会有一个指针,而你想要调用一个带引用的函数.您仍然需要检查空指针,但现在您在调用站点而不是函数执行此操作.我不明白这是如何使代码分析更容易的.我只提出这个因为它让我咬了一次,并且由于隐含的假设参考总是好的,所以追踪它是一个bug. (4认同)
  • @Insilico:那个黑客会导致UB,所以他们得到了他们应得的. (3认同)
  • 使用引用的一个好处是调用者不能传入空值(C++引用不能为null,除非调用者故意破坏它),因此在赋值之前的空指针检查对于out参数是不必要的. (2认同)
  • 您还需要分析代码路径以确保您没有将取消引用的空指针传递给带引用的函数,因此您没有获得太多收益. (2认同)
  • @In silico,你说的是理论而我正在谈论练习.调试错误的引用要比糟糕的指针困难得多.有时,进行一些调试检查以确保合同得到遵守是有用的; 指针很容易,引用几乎不可能. (2认同)
  • @Ben:如果使用指针是流行的做法(但不是),它会违反DRY规则.传递指针现在是规则的例外,因此当您使用它们时,您需要检查您的使用情况.它就像一个面向外部的API而不是面向内部的API.外部输入需要进行验证,但是一旦进入库内部,就不再需要检查.通过将外部API缩小到极小的指针大小,我们实际上将重复的代码减少到只有少数几个地方(指针传递的地方),而不是到处都是. (2认同)

Gre*_*ill 18

在我看来,正确使用const将(大部分)消除对该提示的需要.仍然看起来有用的部分是在阅读调用者代码时,看到:

Func1(x);
Run Code Online (Sandbox Code Playgroud)

目前尚不清楚正在做什么x(特别是像一个不起眼的名字一样Func1).而是使用:

Func2(&x);
Run Code Online (Sandbox Code Playgroud)

使用上述约定,向调用者指示他们应该期望x被修改.

  • @fbrerto - 有时传递NULL是这些方法的_advantage_.有时候你并不总是关心返回值,传递NULL是让函数知道你不需要它的好方法. (5认同)
  • 指针的使用也意味着它可以是"NULL",需要在函数的一侧进行额外的检查. (2认同)

Pet*_*der 11

虽然我不会用尖端的忠告我自己,理由是有效的,这就是为什么像C#语言介绍了outref关键字在调用现场使用.

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


And*_*mas 10

如果你还没有,请购买一份Herb Sutter和Andrei Alexandrescu的"C++编码标准:101规则,指南和最佳实践".阅读.推荐给你的同事.它是本地编码风格的良好基础.

在第25条中,作者建议:

"如果需要参数,则首选通过引用传递,函数不会存储指向它的指针或以其他方式影响其所有权.这表明该参数是必需的,并使调用者负责提供有效的对象."

"Argument is required"表示NULL不是有效值.

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

因此,您需要权衡 - 消除频繁的错误来源,或通过函数名以外的方式确保调用代码的可理解性.我个人倾向于消除风险.


Mar*_*som 7

编码标准基于习惯和常识.你的一些同事可能依赖于多年来根深蒂固的假设,即指针未传递的参数不会改变 - 对它们表示同情.

编码标准的重要部分并不是它们是最优的,而是每个人都遵守它们,以便与代码体保持一致.

  • 我同意这个答案,至少部分是这样.我花了一年多的时间,大量的争论,以及至少三次单独的修订,以使我的上一个团队的编码标准与良好的现代开发实践保持一致.对我们这样的标准进行更改的最佳方法之一(当然,我认为)是编写违反当前标准的代码,然后在代码审查中捍卫为什么您的代码比您拥有的代码更好如果你遵循标准,写.显然你不想成为一个屁股并且一直这样做,但如果你有机会,那就去吧. (3认同)
  • 我同意编码标准是好的,需要遵循.但我认为所有标准都应该是一个活生生的呼吸文件(而不是停滞不前的野兽).我们必须反对机器并提出我们最好的论据.最终我们需要遵循党派路线(因为否则会造成混乱),但这并不妨碍我们试图影响党和教育我们的大学并将他们带入我们所见过的新时代但尚未被他们瞥见了. (2认同)

小智 7

如果他们真的想要在呼叫站点明确提到输出参数,他们实际上应该要求而不是通过尝试使指针表示他们没有的东西来攻击它.指针不仅仅意味着修改比引用更多,并且为非修改对象传递指针并不罕见.

明确表达参数的一种可能方式:

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';
}
Run Code Online (Sandbox Code Playgroud)

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

// in class definition
operator T*() { return &base; }
operator T&() { return base; }

// elsewhere
void old(int *p);
void g() {
  int n;
  old(out(n));
}
Run Code Online (Sandbox Code Playgroud)

我继续编写了这个所需的各种类,以及in-out参数,以一种应该很好地降级的方式.我怀疑我很快就会使用这个约定(至少在C++中),但它适用于任何想要使调用站点显式化的人.