如何避免输出参数?

问题描述:

我见过很多参数,使用返回值优于输出参数。我深信避免这些问题的原因,但我发现自己不确定是否遇到了无法避免的情况。如何避免输出参数?

第一部分我的问题是:什么是你最喜欢/常见的方式来解决使用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作为可能的解决方案,但我需要更多地阅读它,它听起来像一个编译器特定的功能。

+0

非const引用参数的语义是它将(可能)在函数中被更改。毕竟,如果你不打算改变它,你会把它作为一个const引用传递给它。 – Bill 2010-01-26 17:06:57

+0

*“但我很感兴趣,看看其他语言是否便于安装”* - 像Java和C#这样的语言根本没有这个问题,因为他们总是将引用交给对象。 – 2010-01-26 17:13:57

+0

来自每个人的精彩回应。我有一种感觉,在我的具体例子中,没有其他选择。我已经看到来自那些坚定不移,不惜一切代价避免参数的人的SO上的贴子。我希望他们中的一个能详细说明他们如何实现这样的壮举。 至于RVO,我对使用编译器特定的功能有些犹豫,因为它很难向我的老板解释为什么我需要重新设计一半我的代码,因为它不再工作,因为由于缺少一个功能编译器更改/升级。 – 2010-01-27 08:15:38

我不知道为什么你试图避免在这里传递引用。这几乎是存在传递引用语义的情况。

代码

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

看起来完全没有给我。

如果你真的想那么修改你有两个选择

  1. 移动的doWork所以这是它的ExpensiveCopy的成员(你说你不能这样做,所以这是出)
  2. 从doWork返回一个(智能)指针,而不是复制它。
  3. 依靠静脉阻塞(其中其他人指出的是几乎所有的现代编译器支持)
+0

同意。只有一件事,你提到的选项1不可用,因为OP说'ExpensiveCopy'不能修改。 – stakx 2010-01-26 16:43:14

+0

@stakx,你是对的,我错过了。将修复 – Glen 2010-01-26 16:46:17

+0

@stakx:我知道OP说他不能修改这个类。但为什么他不能继承它并在那里添加doWork()方法? – slebetman 2010-01-26 17:07:40

除非你打算(你,你想保持在堆栈上的东西不想做)放下“一切都是不可变的”路线,这与C++不太一样。你不能轻易避开出参数。 C++标准库使用它们,对我来说足够好。

+0

我完全不同意上一条语句:不幸的是,C++标准库并不理想(只是从我的头顶开始:std :: string成员疯狂,过度精细的流等)。我不想开始holiwar,但我猜想这是不是最好的主意,因为众所周知的例子证明一些事情是合理的。 – 2010-01-26 16:53:48

+2

当然,它有它的疣,但总的来说,我发现它非常强大和易于使用 - 我只希望我自己的代码设计得很好。 – 2010-01-26 16:58:36

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

每一个有用的编译器静脉阻塞(返回值优化)如果启用优化,从而有效以下不会导致复制:

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参数和引用。

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

当然,确实有些情况下,出于性能或其他原因不能复制对象。然后Neil's answer适用。

你在做什么平台?

我问的原因是,很多人都提出了Return Value Optimization,这是几乎每个编译器都提供的非常方便的编译器优化。此外,微软和英特尔实施他们称之为命名返回值优化的方法,这更加方便。

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

在命名返回值优化中,您可以按名称返回一个值,编译器将执行相同的操作。 NRVO的好处是你可以在创建的值上进行更复杂的操作(比如调用函数),然后返回它。

尽管如果返回的数据非常大,这两种方法都不能真正消除昂贵的副本,但它们确实有所帮助。

在避免复制方面,唯一真正的方法是使用指针或引用,因为你的函数需要修改你想要的地方的数据。这意味着你可能想要一个传递参考参数。

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

除了所有的意见在这里我想提的是C++ 0x中你很少使用输出参数进行优化的目的 - 因为移动构造函数(见here

+0

我最近尝试建立一个支持使用移动语义一次的文件读取的系统。工作很好,但由于与不同编译器不兼容,最终被烧毁。必须改变这一切,出参数是一个真正的失望。 – 2010-01-27 08:20:23

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

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

在某些情况下,返回值会被重载。在C中,getchar()返回一个int,其思想是存在比char更多的int值(在我所知的所有实现中都是true,在一些我可以轻易想象的false中),所以其中一个值可以用来表示end-的文件。 atoi()返回一个整数,或者是由它传递的字符串表示的整数,如果没有,则返回零,因此它返回“0”和“青蛙”相同的东西。 (如果你想知道是否有一个int值,使用strtol(),它有一个输出参数。)

总是有一种技术在发生错误时抛出异常,但不是所有的多重返回值是错误的,并不是所有的错误都是例外。

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

问问自己为什么你有一些方法可以在这个昂贵的拷贝对象上执行工作。假如你有一棵树,你会把这棵树送进某种建筑方法,或者给树建立自己的建筑方法吗?像这样的情况不断出现,当你有一点点的设计,但往往折叠到自己的时候,你有它的优势。

我知道在实际中,我们并不总是会根本改变每一个对象,但传入参数是一种副作用操作,它使得更难以弄清楚发生了什么,并且你从来没有真正拥有过(除非被他人的代码框架所强制)。 (如果你已经经历了几个大型项目,其中总会有半打出来的参数,那么你就会明白我的意思了),但这绝对不是理想的选择。