从c++函数中返回多个值的首选方法是什么?例如,假设有一个函数对两个整数进行除法并同时返回商和余数。我经常看到的一种方法是使用引用形参:

void divide(int dividend, int divisor, int& quotient, int& remainder);

一种变体是返回一个值,并通过引用形参传递另一个值:

int divide(int dividend, int divisor, int& remainder);

另一种方法是声明一个结构体来包含所有的结果并返回:

struct divide_result {
    int quotient;
    int remainder;
};

divide_result divide(int dividend, int divisor);

这些方法中是否有一种通常是首选的,还是有其他的建议?

编辑:在实际代码中,可能会有两个以上的结果。它们也可能是不同类型的。


当前回答

在C(以及c++)标准中,有使用div、ldiv(以及C99中的lldiv)函数从<stdlib.h>(或<cstdlib>)返回结构的先例。

“返回值和返回参数的混合”通常是最不干净的。

让函数返回状态并通过返回参数返回数据在C中是合理的;在c++中,你可以使用异常来传递失败信息,这就不那么明显了。

如果有两个以上的返回值,那么类结构的机制可能是最好的。

其他回答

如果只有几个返回值,我只会通过引用来做,但对于更复杂的类型,你也可以这样做:

static struct SomeReturnType {int a,b,c; string str;} SomeFunction()
{
  return {1,2,3,string("hello world")}; // make sure you return values in the right order!
}

如果返回类型只是一个临时返回类型,则使用“static”将返回类型的范围限制在这个编译单元。

 SomeReturnType st = SomeFunction();
 cout << "a "   << st.a << endl;
 cout << "b "   << st.b << endl;
 cout << "c "   << st.c << endl;
 cout << "str " << st.str << endl;

这绝对不是最漂亮的方法,但它是有效的。

有很多方法可以返回多个参数。我会讲得很详尽。

使用参考参数:

void foo( int& result, int& other_result );

使用指针参数:

void foo( int* result, int* other_result );

这样做的好处是,您必须在呼叫站点做一个&,可能会提醒人们这是一个out-parameter。

写出一个out<?>模板并使用它:

template<class T>
struct out {
  std::function<void(T)> target;
  out(T* t):target([t](T&& in){ if (t) *t = std::move(in); }) {}
  out(std::optional<T>* t):target([t](T&& in){ if (t) t->emplace(std::move(in)); }) {}
  out(std::aligned_storage_t<sizeof(T), alignof(T)>* t):
    target([t](T&& in){ ::new( (void*)t ) T(std::move(in)); } ) {}
  template<class...Args> // TODO: SFINAE enable_if test
  void emplace(Args&&...args) {
    target( T(std::forward<Args>(args)...) );
  }
  template<class X> // TODO: SFINAE enable_if test
  void operator=(X&&x){ emplace(std::forward<X>(x)); }
  template<class...Args> // TODO: SFINAE enable_if test
  void operator()(Args...&&args){ emplace(std::forward<Args>(args)...); }
};

然后我们可以做:

void foo( out<int> result, out<int> other_result )

一切都很好。Foo不再能够读取任何作为奖励传递进来的值。

其他定义点的方法可以把数据用构造出来。例如,将东西放置在某个地方的回调。

我们可以返回一个结构:

struct foo_r { int result; int other_result; };
foo_r foo();

whick在每个版本的c++中都可以正常工作,在c++17中这也允许:

auto&&[result, other_result]=foo();

零成本。由于保证省略,参数甚至不能移动。

我们可以返回std::tuple:

std::tuple<int, int> foo();

它的缺点是参数没有命名。这允许c++17:

auto&&[result, other_result]=foo();

也在c++17之前,我们可以这样做:

int result, other_result;
std::tie(result, other_result) = foo();

这就有点尴尬了。但是,保证省略在这里不起作用。

进入更陌生的领域(这是在out<>!)

我们可以使用延续传递样式:

void foo( std::function<void(int result, int other_result)> );

现在呼叫者会这样做:

foo( [&](int result, int other_result) {
  /* code */
} );

这种风格的一个好处是你可以返回任意数量的值(具有统一类型),而不必管理内存:

void get_all_values( std::function<void(int)> value )

当你get_all_values([&](int值){})时,值回调可以被调用500次。

对于纯粹的精神错乱,你甚至可以在延续上使用延续。

void foo( std::function<void(int, std::function<void(int)>)> result );

其用途如下:

foo( [&](int result, auto&& other){ other([&](int other){
  /* code */
}) });

这就允许结果和他者之间存在多一关系。

同样使用统一的值,我们可以这样做:

void foo( std::function< void(span<int>) > results )

在这里,我们用一组结果调用回调函数。我们甚至可以重复这样做。

使用这个函数,您可以高效地传递兆字节的数据,而无需对堆栈进行任何分配。

void foo( std::function< void(span<int>) > results ) {
  int local_buffer[1024];
  std::size_t used = 0;
  auto send_data=[&]{
    if (!used) return;
    results({ local_buffer, used });
    used = 0;
  };
  auto add_datum=[&](int x){
    local_buffer[used] = x;
    ++used;
    if (used == 1024) send_data();
  };
  auto add_data=[&](gsl::span<int const> xs) {
    for (auto x:xs) add_datum(x);
  };
  for (int i = 0; i < 7+(1<<20); ++i) {
    add_datum(i);
  }
  send_data(); // any leftover
}

现在,std::function对此有点重,因为我们将在零开销无分配环境中执行此操作。我们想要一个从不分配的function_view。

另一个解决方案是:

std::function<void(std::function<void(int result, int other_result)>)> foo(int input);

foo不是接受回调函数并调用它,而是返回一个接受回调函数的函数。

foo(7)([&](int result, int other_result){ /* code */ });

这通过使用单独的括号将输出参数与输入参数分开。

使用发电机:

使用variant和c++20协程,可以使foo成为返回类型的变体(或仅返回类型)的生成器。语法还没有固定,所以我就不举例子了。

使用信号/槽样式:

在信号和槽的世界中,公开一组信号的函数:

template<class...Args>
struct broadcaster;

broadcaster<int, int> foo();

允许你创建一个异步工作的foo,并在它完成时广播结果。

使用管道:

沿着这条线,我们有各种各样的管道技术,其中一个函数不做任何事情,而是以某种方式安排数据连接,并且这种操作是相对独立的。

foo( int_source )( int_dest1, int_dest2 );

然后,在int_source提供整数之前,这段代码不会执行任何操作。当它这样做时,int_dest1和int_dest2开始接收结果。

就我个人而言,我通常不喜欢返回参数,原因有很多:

在调用中,哪些参数是in,哪些是out并不总是很明显 您通常必须创建一个局部变量来捕获结果,而返回值可以内联使用(这可能是也可能不是一个好主意,但至少您可以选择) 对我来说,一个函数有一个“进”和一个“出”似乎更干净——所有的输入都在这里,所有的输出都在那里 我喜欢让我的论点列表尽可能简短

我对pair/tuple技术也有一些保留意见。主要是,返回值通常没有自然的顺序。代码的读者如何知道是否有结果。首先是商还是余数?实现者可以改变顺序,这将破坏现有的代码。如果值是相同的类型,因此不会生成编译器错误或警告,那么这种情况尤其危险。实际上,这些参数也适用于返回参数。

下面是另一个代码示例,这个示例不那么简单:

pair<double,double> calculateResultingVelocity(double windSpeed, double windAzimuth,
                                               double planeAirspeed, double planeCourse);

pair<double,double> result = calculateResultingVelocity(25, 320, 280, 90);
cout << result.first << endl;
cout << result.second << endl;

这是打印地速和航向,还是航向和地速?这并不明显。

与此相比:

struct Velocity {
    double speed;
    double azimuth;
};
Velocity calculateResultingVelocity(double windSpeed, double windAzimuth,
                                    double planeAirspeed, double planeCourse);

Velocity result = calculateResultingVelocity(25, 320, 280, 90);
cout << result.speed << endl;
cout << result.azimuth << endl;

我想这更清楚了。

所以我认为我的第一选择,一般来说,是结构技术。在某些情况下,对/元组思想可能是一个很好的解决方案。我希望尽可能避免返回参数。

为什么坚持使用具有多个返回值的函数?在OOP中,你可以使用一个类,它提供一个常规函数和一个单一的返回值,以及如下所示的任意数量的额外“返回值”。这样做的好处是调用者可以选择查看额外的数据成员,但并不要求必须这样做。对于复杂的数据库或网络调用,这是首选的方法,在这些调用中,如果发生错误,可能需要大量额外的返回信息。

为了回答您最初的问题,这个示例有一个返回商的方法,这是大多数调用者可能需要的,此外,在方法调用之后,您可以获得作为数据成员的余数。

class div{
   public:
      int remainder;

      int quotient(int dividend, int divisor){
         remainder = ...;
         return ...;
      }
};

使用结构体或类作为返回值。使用std::pair可能暂时有效,但是

如果你决定以后要返回更多信息,这是不灵活的; 从函数头中的声明来看,不太清楚返回的是什么以及返回的顺序。

Returning a structure with self-documenting member variable names will likely be less bug-prone for anyone using your function. Putting my coworker hat on for a moment, your divide_result structure is easy for me, a potential user of your function, to immediately understand after 2 seconds. Messing around with ouput parameters or mysterious pairs and tuples would take more time to read through and may be used incorrectly. And most likely even after using the function a few times I still won't remember the correct order of the arguments.