如何避免输出参数?

时间:2010-01-26 16:34:05

标签: c++ oop function parameters out-parameters

我见过很多论据,使用返回值比out参数更可取。我确信为什么要避免它们,但我发现自己不确定自己是否遇到了无法避免的情况。

第一部分我的问题是:您最喜欢/常用的方法是使用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作为一种可能的解决方案,但我需要阅读更多内容,它听起来像编译器特有的功能。

9 个答案:

答案 0 :(得分:7)

我不确定你为什么要避免在这里传递引用。这些情况几乎都存在传递引用语义。

代码

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

对我来说非常好。

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

  1. 移动doWork,这样它就是ExpensiveCopy的成员(你说你做不到,所以就这样了)
  2. 从doWork返回(智能)指针而不是复制它。 (你不想这样做,因为你想把东西放在堆栈上)
  3. 依靠RVO(其他人已经指出几乎所有现代编译器都支持)

答案 1 :(得分: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参数。

答案 2 :(得分:2)

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

当然,确实存在因性能或其他原因无法复制对象的情况。然后Neil's answer适用。

答案 3 :(得分:2)

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

答案 4 :(得分:1)

除非你要遵循“一切都是不可改变的”路线,否则与C ++的关系不太好。你不能轻易避免出参数。 C ++标准库使用它们,对我来说这对它来说足够好。

答案 5 :(得分:0)

关于你的第一个例子:return value optimization通常会允许直接就地创建返回的对象,而不必复制对象。所有现代编译器都这样做。

答案 6 :(得分:0)

你在做什么平台?

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

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

在命名返回值优化中,您可以按名称返回值,编译器也会执行相同的操作。 NRVO的优势在于,您可以在返回之前对创建的值执行更复杂的操作(如调用函数)。

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

在避免复制方面,唯一真正的方法是使用指针或引用,因为你的函数需要在你希望它最终进入的地方修改数据。这意味着你可能想要传递-by-reference参数。

另外,我想我应该指出,传递引用在高性能代码中非常常见,特别是这个原因。复制数据可能非常昂贵,而且在优化代码时通常会忽略这些数据。

答案 7 :(得分:0)

据我所知,偏好返回值到out参数的原因是它更清晰,并且它适用于纯函数编程(如果函数仅依赖于输入参数,则可以得到一些很好的保证,返回一个值,并没有副作用)。第一个原因是风格,在我看来并不是那么重要。第二个不适合C ++。因此,我不会试图扭曲任何东西以避免输出参数。

一个简单的事实是,一些函数必须返回多个东西,而在大多数语言中,这表明了参数。 Common Lisp有multiple-value-bindmultiple-value-return,其中绑定提供符号列表,并返回值列表。在某些情况下,函数可以返回复合值,例如将被解构的值列表,并且C ++函数返回std::pair并不是什么大问题。在C ++中以这种方式返回两个以上的值会变得很尴尬。始终可以定义结构,但定义和创建结构通常比输出参数更加混乱。

在某些情况下,返回值会超载。在C中,getchar()返回一个int,其思想是有更多的int值而不是char(在我知道的所有实现中都是true,在我可以轻易想象的某些实现中是false),因此可以使用其中一个值表示文件结尾。 atoi()返回一个整数,或者是由传递的字符串表示的整数,如果没有则返回零,所以它返回“0”和“frog”的相同内容。 (如果你想知道是否有int值,请使用strtol(),它有一个out参数。)

在出现错误的情况下始终存在抛出异常的技术,但并非所有多个返回值都是错误,并且并非所有错误都是例外。

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

答案 8 :(得分:0)

问问自己为什么你有一些方法可以在这个昂贵的复制对象上执行工作。假设你有一棵树,你会把树送到一些建筑方法中,或者给树树做自己的建筑方法吗?当你有一点点设计时,这样的情况会不断出现,但当你拍下它时,往往会折叠成自己。

我知道实际上我们并不总是能够改变每一个对象,但是输入参数是一个副作用操作,这使得弄清楚发生了什么变得更加困难,而你从来没有真正需要这样做(除非通过在其他人的代码框架内工作而强迫)。

有时它会更容易,但绝对不可能无缘无故地使用它(如果你经历了一些大型项目,那里总有六个参数,你会知道我的意思)。