输出参数并通过引用传递

时间:2010-10-26 22:02:44

标签: c++ pointers reference coding-style

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

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

争论的准则是:

  

提示:使用指针代替引用返回参数。

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

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

10 个答案:

答案 0 :(得分:31)

  

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

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

  

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

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

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

  

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

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

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

  

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

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

  

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

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


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

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

答案 1 :(得分:17)

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

Func1(x);

目前还不清楚使用x做了什么(尤其是像Func1这样的非描述性名称)。而是使用:

Func2(&x);

使用上述约定,向调用者表明他们应该期望修改x

答案 2 :(得分:11)

虽然我不会自己使用提示的建议,但理由是有效的,这就是为什么像C#这样的语言引入了outref关键字以供在呼叫站点使用。

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

答案 3 :(得分:10)

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

在规则25中,作者建议:

  

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

“Argument is required”表示NULL不是有效值。

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

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

答案 4 :(得分:7)

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

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

答案 5 :(得分: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';
}

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

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

// elsewhere
void old(int *p);
void g() {
  int n;
  old(out(n));
}

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

答案 6 :(得分:2)

我发现有两个关于这个的学校:

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

我同意你对(a)的动机:在阅读代码时,你无法知道所有的声明,即使鼠标悬停为你提供了函数的声明。将成千上万行中的数百个函数鼠标放在上面只需要时间。

如果你混合输入和输出参数,我肯定会在这里看到一个问题:

bool GetNext(int index, Type & result);

对这种愚蠢的召唤看起来像这样:

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

在该示例中,调用本身非常明显,可能会修改t。但是index呢?也许GetNext递增索引,所以你总是得到下一个项目,而被调用者不需要保持调用者状态?

这通常会引发回复然后方法应该是GetNextAndIncrementIndex,或者你应该使用迭代器。我敢打赌,这些人从来不必调试电气工程师编写的代码,他们仍然认为 Numerical Recipes 是编程的圣杯。

Howver 我仍倾向于(b):仅仅因为编写新代码可以避免问题,“可能无效或无效”通常是更常见的问题。

答案 7 :(得分:1)

理由在逻辑上是正确的 编码人员可能会对价值发生变化感到惊讶(因为他们认为价值是通过价值传递的)。

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

答案 8 :(得分:1)

我不同意这条准则。通过确保代码是const-correct,可以很容易地解决理由中提到的混淆。如果要通过引用将输入参数传递给函数,那么它应该是const引用。如果引用不是const,则表明它是输出参数,其值可能会被函数更改。

此外,当您将指针传递给函数而不是引用时,会立即提出一个问题,即这是否是指向动态分配的内存的指针,以及是否应该释放它。使用引用消除了调用delete的诱惑。

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

答案 9 :(得分:1)

我建议:

  • 通过引用传递(不要通过指针)
  • 尽可能传递const引用(假设您已在整个代码库中正确使用了const)
  • 放置在列表的开头
  • 处变异的参数/参数
  • 正确标记该功能
  • 适当地标记参数(并使用详细的描述性名称和少量参数创建方法/函数)
  • 记录结果
  • 如果多个参数/参数发生变异,请考虑创建一个包含这些参数的简单类(即使是通过引用本身)
  • 如果在没有可视化和文档化提示的情况下它们仍然无法运行(sic),请为变量创建一个轻量级模板容器对象,然后将其传递给方法或函数