从C++函数返回多个值

366

从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);

这些方式中是否有一种通常更受青睐,或者还有其他建议?

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

23个回答

328

在C++11中,你可以:

#include <tuple>

std::tuple<int, int> divide(int dividend, int divisor) {
    return  std::make_tuple(dividend / divisor, dividend % divisor);
}

#include <iostream>

int main() {
    using namespace std;

    int quotient, remainder;

    tie(quotient, remainder) = divide(14, 3);

    cout << quotient << ',' << remainder << endl;
}

在 C++17 中:

#include <tuple>

std::tuple<int, int> divide(int dividend, int divisor) {
    return  {dividend / divisor, dividend % divisor};
}

#include <iostream>

int main() {
    using namespace std;

    auto [quotient, remainder] = divide(14, 3);

    cout << quotient << ',' << remainder << endl;
}

或者使用结构体:

auto divide(int dividend, int divisor) {
    struct result {int quotient; int remainder;};
    return result {dividend / divisor, dividend % divisor};
}

#include <iostream>

int main() {
    using namespace std;

    auto result = divide(14, 3);

    cout << result.quotient << ',' << result.remainder << endl;

    // or

    auto [quotient, remainder] = divide(14, 3);

    cout << quotient << ',' << remainder << endl;
}

7
对于函数返回元组的情况,我有一个疑虑。假设上述函数原型在头文件中,那么我如何知道第一个和第二个返回值的含义,而不了解函数定义呢?是商和余数还是余数和商? - Uchia Itachi
20
对于函数参数也有同样的问题,你可以为它们赋予名字,但语言并没有强制要求,并且在阅读时,参数名称在调用站点没有价值。此外,在单个返回值中,你只有一个类型,但是具有名称也可能很有用,对于元组,这个问题会变成两倍,所以我认为,该语言在多种方式上都存在自我记录不足的问题,不仅仅是这个。 - oblitum
1
如果我想明确指定divide()的返回类型,最后一个示例会是什么样子?我应该在其他地方定义结果,还是可以在返回类型规范中直接定义它? - Slava
3
@Slava,你不能在函数签名中定义类型,你需要在函数体外声明类型并将其用作返回类型,就像通常做的那样(只需将struct行移到函数体外部,并将auto函数返回替换为result)。 - oblitum
4
如果想将divide函数的定义放入一个单独的cpp文件中,我遇到了错误error: use of ‘auto divide(int, int)’ before deduction of ‘auto’。我该如何解决? - Adriaan
显示剩余3条评论

282

为了返回两个值,我使用std::pair(通常通过typedef)。如果要返回多于两个结果,请考虑使用boost::tuple(在C++11及更高版本中有std::tuple)。

随着C++17引入结构化绑定,返回std::tuple可能会成为被广泛接受的标准。


17
+1 表示元组。请注意大对象返回结构与通过引用传递之间的性能影响。 - Marcin
15
如果你要使用元组,为什么不同时将其用于成对的情况呢?为什么要有一个特殊情况? - Ferruccio
4
弗雷德,是的,boost::tuple可以做到那个 :) - Johannes Schaub - litb
55
在C++11中,您可以使用std::tuple - Ferruccio
17
如果您想从函数中接收多个值,一种方便的方法是使用std::tie。https://dev59.com/L3E85IYBdhLWcg3w3Xlp#2573822 - fdermishin
显示剩余6条评论

155

就我个人而言,我通常不喜欢使用返回参数,原因如下:

  • 在调用时,哪些参数是输入和哪些参数是输出并不总是很明显
  • 通常需要创建一个本地变量来接收结果,而返回值可以内联使用(这可能是好或坏的,但至少你有这个选项)
  • 对于函数而言,我认为有一个“进门”和一个“出门”更清晰——所有的输入都在这里,所有的输出都从那里出来
  • 我喜欢尽可能使我的参数列表短

我也对成对/元组技术有一些保留意见。主要问题是返回值通常没有自然顺序。代码读者该怎么知道result.first是商还是余数?实现者可能会改变顺序,这将破坏现有代码。如果这些值是相同类型的,则没有编译器错误或警告,这尤其危险。实际上,这些论点也适用于返回参数。

这里是另一个代码示例,稍微复杂一些:

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;

我认为这更清晰易懂。

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


1
struct声明为Velocity的建议很好。但是,一个担忧是它会污染命名空间。我想,使用C++11,struct可以有一个长类型名称,可以使用auto result = calculateResultingVelocity(...) - Hugues
9
一个函数应该只返回一个“东西”,而不是一些按某种方式排序的“东西元组”。 - DevSolar
1
我更喜欢使用结构体而不是std::pairs/std::tuples,原因在于这个答案中所描述的。但我也不喜欢"污染"命名空间。对我来说,理想的解决方案是返回匿名结构体,如struct { int a, b; } my_func();。这可以像这样使用:auto result = my_func();。但C++不允许这样做:"不能在返回类型中定义新类型"。所以我必须创建类似于struct my_func_result_t的结构体... - anton_rh
3
C++14支持使用auto返回局部类型,因此可以轻松地获得auto result = my_func(); - ildjarn
@ildjarn 看起来它可以工作:http://coliru.stacked-crooked.com/a/2a80e1007ff8804e。但是函数体必须在头文件中定义。 - anton_rh
12
大约15年前,当我们发现boost后,由于它非常方便,我们经常使用元组。随着时间的推移,我们发现了阅读性方面的不足,特别是对于具有相同类型的元组(例如tuple<double,double>;哪个是哪个)。因此,最近,我们更习惯于引入一个小的POD结构体,其中至少成员变量的名称表示某些合理的意义。 - gast128

35

有很多方法可以返回多个参数。我将详细说明。

使用引用参数:

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

使用指针参数:

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

这种方法的优点是在调用时必须使用&,可能会提醒人们这是一个输出参数。

编写一个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不能再读取任何作为奖励传递的值。

其他定义可以放置数据的方法可用于构造out。例如,回调以将事物放置在某个位置。

我们可以返回一个结构:

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

这在每个版本的C++中都能正常工作,在中还允许:

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

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

我们可以返回一个std::tuple

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

这种方法的缺点是参数没有名称。这使得成为可能:

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

同样地,在之前,我们可以这样做:

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 value){} ) 时,value 回调可能会被调用500次。

为了纯粹的疯狂,甚至可以在 continuation 上使用 continuation。

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

使用方式如下:

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

这将允许resultother之间的多对一关系。

再次使用统一值,我们可以这样做:

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协程,您可以将foo作为返回类型变体(或只是返回类型)的生成器。 语法尚未确定,因此我不会给出示例。

使用信号/插槽风格:

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

template<class...Args>
struct broadcaster;

broadcaster<int, int> foo();

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

使用管道:

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

foo( int_source )( int_dest1, int_dest2 );

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

1
这个答案包含比其他答案更多的信息!特别是关于返回元组和结构体的函数中 auto&&[result, other_result]=foo(); 的信息。谢谢! - jjmontes
1
我很感激这个详尽的答案,特别是因为我仍然被困在C++11中,因此无法使用其他人提出的一些更现代的解决方案。 - Bri Bri
auto&&[result, other_result]=foo(); 中,为什么要使用 auto&& 而不是 auto - starriet
@starriet auto& 表示“可修改引用”,auto const& 表示“不可修改引用,可能是扩展临时变量”,auto 表示“复制一份”。我使用 auto&& 表示“我不在乎,你帮我解决,可以是具有生命周期延长的临时变量,也可以是引用,不是我的问题,我对任何东西都满意”。 - Yakk - Adam Nevraumont
@Yakk-AdamNevraumont 谢谢。我以为 T&& 表示右值引用,所以我认为使用 auto&auto&& 而不是 auto(拷贝) 会产生悬空引用。使用 auto&auto&& 捕获函数的返回值是否可以?另外,我不明白“我不在乎,你帮我解决”怎么能行。如果我理解错了什么,请告诉我,我正在努力学习这些概念。谢谢 :) - starriet

30
std::pair<int, int> divide(int dividend, int divisor)
{
   // :
   return std::make_pair(quotient, remainder);
}

std::pair<int, int> answer = divide(5,2);
 // answer.first == quotient
 // answer.second == remainder

std::pair本质上是您的结构体解决方案,但已为您定义并准备好适应任何两种数据类型。


3
可以,我的简单示例可以这样做。然而一般情况下,可能会返回超过两个值。 - Fred Larson
6
不是自我说明的。你能记得x86寄存器DIV操作的余数是哪一个吗? - Mark
2
@Mark - 我同意,位置解决方案可能会更难维护。你可能会遇到“排列和困惑”问题。 - Fred Larson

17

这完全取决于实际函数和多个值的含义,以及它们的大小:

  • 如果它们像分数示例中一样相关,则应使用结构体或类实例。
  • 如果它们不相关且不能组合成类/结构体,则建议将该方法重构为两个。
  • 根据您返回的值的内存大小,您可能需要返回指向类实例或结构体的指针,或使用引用类型的参数。

1
我喜欢你的回答,你最后提到的一点让我想起了我刚读到的一篇文章,它说根据情况传值已经变得更快了,这使得问题更加复杂... http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/ - sage

15

使用C++17,您还可以返回一个或多个不可移动/不可复制的值(在某些情况下)。通过新的保证返回值优化,返回不可移动类型的可能性得以实现,并且与聚合体和所谓的模板构造函数很好地组合。

template<typename T1,typename T2,typename T3>
struct many {
  T1 a;
  T2 b;
  T3 c;
};

// guide:
template<class T1, class T2, class T3>
many(T1, T2, T3) -> many<T1, T2, T3>;

auto f(){ return many{string(),5.7, unmovable()}; }; 

int main(){
   // in place construct x,y,z with a string, 5.7 and unmovable.
   auto [x,y,z] = f();
}

这个东西的好处在于它保证不会造成任何拷贝或移动。你也可以将many结构体变量定义为可变参数。更多细节请见:

返回可变聚合体(struct)和C++17可变模板“构造推导指南”的语法


13

针对这个问题,面向对象的解决方案是创建一个比例类。这不需要额外的代码(甚至可以节省一些代码),会更加清晰简洁,并且可以让你进行其他的重构以简化这个类之外的代码。

其实我认为有人建议返回一个结构体,虽然很接近但隐藏了意图——这需要是一个完整设计的类,包括构造函数和一些方法。实际上,你最初提到的“方法”(返回这个组合)很可能应该成为这个类的成员方法,返回一个自身的实例。

我知道你的例子只是一个“例子”,但事实上,除非你的函数做的比任何函数都多,否则如果你想要返回多个值,你几乎肯定缺少一个对象。

不要害怕创建这些小类来完成一些小任务——这就是面向对象的魔力——你可以将它分解成每个方法都非常小而简单,每个类都很小而易懂。

另一个表明有问题的迹象是:在面向对象中,你基本上没有数据——面向对象不是关于传递数据的,类需要在内部管理和操作自己的数据,任何数据传递(包括访问器)都表明你可能需要重新思考一些东西。


请问您能详细解释一下“隐藏意图”吗?如果您能举个例子来说明如何使用面向对象实现OP的代码,那就太好了。 - Sabito stands with Ukraine
1
@Sabito錆兎 Fred Larson的例子很好。 对于像“Divide”这样真正通用的实用方法,面向对象并不总是最好的解决方案,但当您像他一样解决实际业务问题时,优势变得明显。 - Bill K

10

使用C++17, 使用std::make_tuple, 结构化绑定,尽可能使用auto:

#include <tuple>

#include <string>
#include <cstring>

auto func() {
    // ...
    return std::make_tuple(1, 2.2, std::string("str"), "cstr");
}

int main() {
    auto [i, f, s, cs] = func();
    return i + f + s.length() + strlen(cs);
}

使用 -O1 时,代码可以完全进行优化:https://godbolt.org/z/133rT9Pcq
只有当需要优化 std::string 时,才需要 -O3https://godbolt.org/z/Mqbez73Kf

在这里:https://godbolt.org/z/WWKvE3osv,你可以看到 GCC 将所有返回值打包在单个内存块(rdi+N)中,像 POD 风格一样,证明没有性能损失。


10

在C(以及因此也是C ++)标准中,使用<stdlib.h>(或<cstdlib>)中的divldiv(以及在C99中的lldiv )函数返回结构体已经有先例。

'返回值和返回参数混合'通常是最不清晰的。

在C中,让函数通过返回状态并通过返回参数返回数据是明智的;在C++中,您可以使用异常来传递失败信息,这种方式不太明显可行。

如果有两个以上的返回值,则类似结构体的机制可能是最好的选择。


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