8

我见过很多参数,使用返回值比输出参数更可取。我深信要避免它们的原因,但我发现自己不确定我是否遇到了不可避免的情况。

我的问题的第一部分是:您最喜欢/常用的使用 out 参数的方式有哪些?类似的东西:伙计,在同行评审中,我总是看到其他程序员这样做,而他们本来可以很容易地做到这一点。

我的问题的第二部分涉及我遇到的一些特定情况,我想避免使用 out 参数但想不出一种干净的方法来做到这一点。

示例 1:我有一堂课,我想避免使用昂贵的副本。可以在对象上完成工作,这会使对象的复制变得昂贵。建立数据的工作也不是微不足道的。目前,我将把这个对象传递给一个修改对象状态的函数。这对我来说比在工作函数内部新建对象并将其返回更可取,因为它允许我将东西保留在堆栈上。

class ExpensiveCopy //Defines some interface I can't change.
{
public:
    ExpensiveCopy(const ExpensiveCopy toCopy){ /*Ouch! This hurts.*/ };
    ExpensiveCopy& operator=(const ExpensiveCopy& toCopy){/*Ouch! This hurts.*/};

    void addToData(SomeData);
    SomeData getData();
}

class B
{
public:
    static void doWork(ExpensiveCopy& ec_out, int someParam);
    //or
    // Your Function Here.
}

使用我的函数,我得到这样的调用代码:

const int SOME_PARAM = 5;
ExpensiveCopy toModify;
B::doWork(toModify, SOME_PARAM);

我想要这样的东西:

ExpensiveCopy theResult = B::doWork(SOME_PARAM);

但我不知道这是否可能。

第二个例子:我有一个对象数组。数组中的对象是一种复杂类型,我需要对每个元素进行处理,我希望将这些工作与访问每个元素的主循环分开。目前的代码如下所示:

std::vector<ComplexType> theCollection;
for(int index = 0; index < theCollection.size(); ++index)
{
    doWork(theCollection[index]);
}

void doWork(ComplexType& ct_out)
{
   //Do work on the individual element.
}

关于如何处理其中一些情况的任何建议?我主要使用 C++ 工作,但我很想看看其他语言是否有助于更轻松的设置。我遇到了 RVO 作为一种可能的解决方案,但我需要阅读更多关于它的内容,它听起来像是一个编译器特定的功能。

4

9 回答 9

7

我不确定您为什么要避免在此处传递引用。传递引用语义几乎就是在这些情况下存在的。

编码

static void doWork(ExpensiveCopy& ec_out, int someParam);

对我来说看起来非常好。

如果你真的想修改它,那么你有几个选择

  1. 移动 doWork 使其成为 ExpensiveCopy 的成员(你说你不能这样做,所以它已经出局了)
  2. 从 doWork 返回一个(智能)指针,而不是复制它。(你不想这样做,因为你想把东西放在堆栈上)
  3. 依赖 RVO(其他人指出几乎所有现代编译器都支持 RVO)
于 2010-01-26T16:37:53.580 回答
3

如果启用优化,每个有用的编译器都会执行 RVO(返回值优化),因此以下内容实际上不会导致复制:

Expensive work() {
    // ... no branched returns here
    return Expensive(foo);
}

Expensive e = work();

在某些情况下,编译器也可以应用 NRVO,命名返回值优化:

Expensive work() {
    Expensive e; // named object
    // ... no branched returns here
    return e; // return named object
}

然而,这并不完全可靠,仅适用于更琐碎的情况并且必须进行测试。如果您无法测试每种情况,只需在第二种情况下使用带有引用的 out-parameters。

于 2010-01-26T16:42:32.117 回答
2

IMO 你应该问自己的第一件事是复制是否ExpensiveCopy真的如此昂贵。要回答这个问题,您通常需要一个分析器。除非分析器告诉您复制确实是一个瓶颈,否则只需编写更易于阅读的代码:ExpensiveCopy obj = doWork(param);.

当然,确实存在出于性能或其他原因无法复制对象的情况。然后尼尔的答案适用。

于 2010-01-26T16:42:38.243 回答
2

除了这里的所有评论,我还要提到在 C++0x 中你很少使用输出参数进行优化——因为移动构造函数(见这里

于 2010-01-26T16:57:55.943 回答
1

除非您走“一切都是不可变的”路线,否则 C++ 不太适合。你不能轻易避免输出参数。C++ 标准库使用它们,对我来说足够好就足够了。

于 2010-01-26T16:38:56.727 回答
0

至于您的第一个示例:返回值优化通常允许直接就地创建返回的对象,而不必复制对象。所有现代编译器都这样做。

于 2010-01-26T16:40:35.880 回答
0

你在哪个平台上工作?

我问的原因是很多人建议返回值优化,这是几乎每个编译器都存在的非常方便的编译器优化。此外,微软和英特尔实施了他们所谓的命名返回值优化,这更加方便。

在标准的返回值优化中,您的返回语句是对对象构造函数的调用,它告诉编译器消除临时值(不一定是复制操作)。

在命名返回值优化中,您可以按名称返回一个值,编译器也会做同样的事情。NRVO 的优点是您可以在返回之前对创建的值执行更复杂的操作(例如对其调用函数)。

如果您返回的数据非常大,虽然这些都不能真正消除昂贵的副本,但它们确实有帮助。

就避免复制而言,唯一真正的方法是使用指针或引用,因为您的函数需要在您希望它结束​​的地方修改数据。这意味着您可能希望通过-参考参数。

另外我想我应该指出,由于这个原因,引用传递在高性能代码中非常常见。复制数据可能非常昂贵,而且人们在优化代码时往往会忽略这一点。

于 2010-01-26T16:55:30.663 回答
0

据我所见,更喜欢返回值而不是输出参数的原因是它更清晰,并且它适用于纯函数式编程(如果函数仅依赖于输入参数、返回值并且具有无副作用)。第一个原因是风格,在我看来并不是那么重要。第二个不适合 C++。因此,我不会试图扭曲任何东西来避免输出参数。

一个简单的事实是,一些函数必须返回多个东西,而在大多数语言中,这建议输出参数。Common Lisp 有multiple-value-bindand multiple-value-return,其中一个符号列表由绑定提供,并返回一个值列表。在某些情况下,一个函数可以返回一个复合值,例如一个值列表,然后将被解构,C++ 函数返回一个std::pair. 在 C++ 中以这种方式返回两个以上的值会很尴尬。总是可以定义一个结构,但定义和创建它通常比输出参数更麻烦。

在某些情况下,返回值会过载。在 C 中,getchar()返回一个 int,其想法是 int 值比 char 多(在我知道的所有实现中为 true,在某些我可以轻松想象的实现中为 false),因此其中一个值可用于表示结束-文件。 atoi()返回一个整数,可以是它所传递的字符串所表示的整数,或者如果没有则为零,因此它为“0”和“frog”返回相同的值。(如果您想知道是否存在 int 值,请使用strtol(),它确实有一个 out 参数。)

总是有在发生错误时抛出异常的技术,但并非所有的多个返回值都是错误,也不是所有的错误都是异常的。

因此,重载的返回值会导致问题,多值返回并不容易在所有语言中使用,并且单个返回并不总是存在。抛出异常通常是不合适的。使用 out 参数通常是最干净的解决方案。

于 2010-01-26T17:02:03.070 回答
0

问问自己,为什么你首先有一些方法可以在这个昂贵的复制对象上执行工作。假设您有一棵树,您会将树送入某种构建方法还是赋予树自己的构建方法?当你的设计有点偏离时,这样的情况会不断出现,但当你把它放下时往往会折叠起来。

我知道实际上我们并不总是可以更改每个对象,但是传入参数是一种副作用操作,它使弄清楚发生了什么变得更加困难,而且你永远不必这样做(除非在其他人的代码框架中强制执行)。

有时它更容易,但绝对不希望无缘无故地使用它(如果您经历过一些大型项目,其中总是有六个参数,您就会明白我的意思)。

于 2010-01-26T19:40:44.713 回答