返回值作为参数传递

3

我来自Java背景,但现在正在处理大型C++代码库。我经常看到这种模式:

void function(int value, int& result);

上述方法的调用方式如下:

int result = 0;
function(42, result);
std::cout << "Result is " << result << std::endl;

在Java中,以下内容更为常见:
int result = function(42);

尽管在C++中以上操作是完全可能的,但为什么前者似乎更常见(至少在我正在工作的代码库中)?这是风格问题还是其他原因?

2
函数 void function(int value, int result); 不会修改传递的 result - MikeCAT
2
我修改了,希望没问题。我相信这就是 OP 所指的模式。 - lubgr
4
“为什么前者看起来更常见?”你对这个说法有什么证据?我不知道具体数字,但我认为输出参数比“正常”的返回值要少得多。如果我没记错的话,C++核心指南也建议避免使用输出参数。 - 463035818_is_not_a_number
3
我现在正在处理大型的C++代码库,为什么前者更常见呢?难道它在那个代码库中更常见吗?为什么不问作者呢?那是哪个代码库? - KamilCuk
2
任何答案都是基于个人意见的,并且具体取决于您的代码库。一般来说,这只是在C++中使用函数的一种方式。有些人讨厌这种模式,有些人喜欢它,大多数人则处于中间状态。 - AndyG
显示剩余3条评论
4个回答

4
首先,这曾经是一种常见的技术,可以让一个函数具有多个输出。例如,在此签名中,
int computeNumberButMightFail(int& error_code);

在过去,为了传递信息和说明错误,我们通常会同时返回 int类型的数据和一些用于标记错误的变量,但现在有更好的方法。如使用 std::optional<T> 作为返回值。另外还可以考虑使用更灵活的 std::expected<T, ...> 或者使用 C++ 标准库提供的 std::make_tuple 返回多个值,然后再用结构化绑定对它们进行解构处理。对于异常错误的情况,通常会使用异常处理机制。

其次,这是一个优化技巧,源于 (N)RVO 不广泛应用的时代:如果函数的输出结果是一个无法复制的耗费大量资源的对象,则需要确保不会进行不必要的拷贝操作:

void fillThisHugeBuffer(std::vector<LargeType>& output);

为了避免在按值返回数据时进行不必要的复制,我们通过引用传递数据的参考。然而,这种方式已经过时,通常被认为直接通过值返回较大的对象是更加惯用的方法。因为C++17保证了临时物体的实现,而且所有主要编译器都实现了名字返回值优化。

另请参阅核心指南: F.20 - “对于“out”输出值,请优先使用返回值而不是输出参数”。


1
据我所知,至少对于基本数据类型作为返回值而言,在C++中这种情况并不常见。有几种情况需要考虑:
  1. 如果你在使用纯C或者非常受限的环境下工作,例如不允许使用C++异常(如实时应用程序),那么函数的返回值通常用于指示函数的成功与否。在C中可以这样写:
#include <stdio.h>
#include <errno.h>

int func(int arg, int* res) {
  if(arg > 10) {
    return EINVAL; //this is an error code from errnoe
  }
  
  ... //do stuff
  *res = my_result;
}

有时候在C++中也会使用这种方法,因此结果必须通过引用/指针进行赋值。

  1. 当你的结果是一个已经存在的结构体或对象,在调用函数之前存在,并且你的函数目的是修改结构体或对象内部属性时,这是一种常见的模式,因为你必须通过引用传递参数(以避免复制)。因此,没有必要返回与传递给函数相同的对象。在C++中的一个例子可能是:
#include <iostream>

struct Point {
  int x = 0;
  int y = 0;
};

void fill_point(Point& p, int x, int y) {
   p.x = x;
   p.y = y;
}

int main() {
  Point p();
  fill_point(p);

  return EXIT_SUCCESS;
}

然而,这只是一个琐碎的问题,有更好的解决方案,比如将填充函数定义为对象中的一个方法。但是,在涉及到对象的单一责任原则时,在更复杂的情况下,这种模式很常见。
在Java中,您无法控制堆。您定义的每个对象都在堆上,并自动通过引用传递给函数。在C++中,您可以选择要存储对象的位置(堆或栈)以及如何将对象传递给函数。重要的是要记住,按值传递对象会将其复制,并且通过值从函数返回对象也会复制该对象。要通过引用返回对象,必须确保其生命周期超出函数范围,方法是将其放在堆上或通过引用将其传递给函数。

1
可修改的参数,接收函数调用的副作用而获得值的,称为输出参数。它们通常被认为有点过时,在C++中已经不再流行,因为有更好的技术可用。正如您所建议的,从函数返回计算出来的值是理想的。
但现实世界中的限制有时会使人们转向输出参数:
  1. 由于复制大对象或具有非平凡复制构造函数的对象的代价昂贵,按值返回对象的成本太高。
  2. 返回多个值,并创建包含这些值的元组或结构可能很麻烦、昂贵或不可能。
  3. 当对象无法被复制(可能为私有或删除的复制构造函数)但必须在原地创建时。
大多数这些问题面对的是遗留代码,因为C++11引入了“移动语义”,C++17引入了“拷贝省略”,这些都消除了大部分这些情况。
在任何新代码中,使用输出参数通常被认为是不好的风格或"代码异味",并且很可能是从过去继承下来的习惯(当时这是一种更相关的技术)。这并不是错误,但如果没有必要,我们尝试避免使用它。

0

在C++代码库中使用out参数的原因有几个。

例如:

  • 您有多个输出:

    void compute(int a, int b, int &x, int &y) { x=a+b; y=a-b; }

  • 您需要返回值用于其他事情:例如,在PEG解析中,您可能会发现以下内容:

    if (parseSymbol(pos,symbolName) && parseToken(pos,"=") && parseExpression(pos,exprNode)) {...}

其中解析函数看起来像是这样的:

bool parseSymbol(int &pos, string &symbolName); 
bool parseToken(int &pos, const char *token); 

等等

  • 为了避免对象副本。

  • 程序员不知道更好的方法。

但基本上我认为,任何答案都是基于观点的,因为这取决于风格和编码政策是否使用或不使用输出参数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接