繁体   English   中英

返回与不返回功能?

[英]Return vs. Not Return of functions?

返回或不返回,这是一个功能问题! 或者,它真的重要吗?


这就是故事 :我曾经写过如下代码:

Type3 myFunc(Type1 input1, Type2 input2){}

但是最近我的项目学院告诉我,我应该尽可能地尝试避免编写这样的函数,并通过将返回的值放在输入参数中来建议以下方法。

void myFunc(Type1 input1, Type2 input2, Type3 &output){}

他们让我确信这是更好更快的,因为在第一种方法中返回时需要额外的复制步骤。


对我来说,我开始相信第二种方法在某些情况下更好,特别是我有多个要返回或修改的东西。 例如:以下第二行将比第一行更好更快,因为避免在返回时复制整个vecor<int>

vector<int> addTwoVectors(vector<int> a, vector<int> b){}
void addTwoVectors(vector<int> a, vector<int> b, vector<int> &result){}:

但是,在其他一些情况下,我不能买它。 例如,

bool checkInArray(int value, vector<int> arr){}

绝对会比

void checkInArray(int value, vector<int> arr, bool &inOrNot){}

在这种情况下,我认为通过直接返回结果的第一种方法在更好的可读性方面更好。


总之,我很困惑(强调C ++):

  • 什么应该由函数返回,什么不应该(或尽量避免)?
  • 我有什么标准的方法或好的建议吗?
  • 我们可以在可读性和代码效率方面做得更好吗?

编辑 :我知道,在某些情况下,我们必须使用其中之一。 例如,如果我需要实现method chaining ,我必须使用return-type functions 因此,请关注可以应用这两种方法来实现目标的情况。

我知道这个问题可能没有一个答案或肯定的事情。 此外,似乎需要在许多编码语言中做出这样的决定,例如CC++等。因此,任何意见或建议都非常受欢迎(更好的例子)。

像往常一样,当有人提出一件事比另一件事快时,你是否采取了时间安排? 在完全优化的代码中,您计划使用的每种语言和每个编译器? 没有它,任何基于性能的论证都没有实际意义。

我将在一秒钟内回到性能问题,让我先解决一下我认为更重要的问题:当然,有充分的理由通过引用传递函数参数。 我现在能想到的主要问题是参数实际上是输入和输出,即该函数应该对现有数据进行操作。 对我来说,这就是采用非const引用的函数签名所表明的。 如果这样的函数然后忽略了该对象中已经存在的东西(或者更糟糕的是,显然希望只得到一个默认构造的那个),那么该接口就会让人困惑。

现在,回到表演。 我不能代表C#或Java(虽然我相信在Java中返回一个对象不会首先导致副本,只是传递一个引用),而在C中,你没有引用但可能需要求助于传递指针周围(然后,我同意传入指向未初始化内存的指针是可以的)。 但是在C ++中,编译器已经做了很长时间的返回值优化,RVO,这基本上只意味着在大多数调用中,如A a = f(b); ,副本构造函数被绕过, f将直接在正确的位置创建对象。 在C ++ 11中,我们甚至使用移动语义来使其显式化并在更多地方使用它。

你应该只返回一个A*吗? 只有你真的渴望过去的手动内存管理。 至少,返回一个std::shared_ptr<A>或一个std::unique_ptr<A>

现在,有了多个输出,当然你会得到额外的复杂功能。 首先要做的是你的设计是否合适:每个函数都应该有一个责任,通常,这意味着返回一个值。 但当然有例外; 例如,分区功能必须返回两个或多个容器。 在这种情况下,您可能会发现使用非const引用参数更容易阅读代码; 或者,你可能会发现返回一个元组是要走的路。

我恳请你们两种方式编写代码,然后在第二天或周末之后回来看看这两个版本。 然后,决定什么更容易阅读。 最后,这是良好代码的主要标准。 对于那些您可以从最终用户工作流程中看到性能差异的地方,这是一个需要考虑的额外因素,但只有在非常罕见的情况下才应该优先于可读代码 - 并且只需要更多的努力,您就可以无论如何通常都要工作。

由于返回值优化,第二种形式(传递引用并对其进行修改)几乎肯定更慢,更难以修改,也不太容易辨认。

让我们考虑一个简单的示例函数:

return_value foo( void );

以下是可能发生的可能性:

  1. 返回值优化(RVO)
  2. 命名返回值优化(NRVO)
  3. 移动语义返回
  4. 复制语义返回

什么是回报值优化 考虑这个功能:

return_value foo( void ) { return return_value(); }

在此示例中,从单个出口点返回未命名的临时变量。 因此,编译器可以轻松(并且可以自由地)完全删除此临时值的任何痕迹,而是在调用函数中直接构造它:

void call_foo( void )
{
    return_value tmp = foo();
}

在这个例子中,tmp实际上直接在foo中使用,就像foo定义它一样,删除所有副本。 如果return_value是非平凡类型,那么这是一个巨大的优化。

什么时候可以使用RVO? 这取决于编译器,但通常,使用单个返回代码点,它将始终使用。 多个返回代码点使它更加不确定,但如果它们都是匿名的,那么你的机会就会增加。

命名返回值优化怎么样?

这个有点棘手; 如果在返回变量之前命名变量,它现在是一个l值。 这意味着编译器必须做更多的工作来证明就地构造是可能的:

return_type foo( void )
{
    return_type bar;
    // do stuff
    return bar;
}

通常,这种优化仍然是可能的,但是对于多个代码路径的可能性较小,除非每个代码路径返回相同的对象; 从多个不同的代码路径返回多个不同的对象往往不难以优化:

return_type foo( void)
{
    if(some_condition)
    {
        return_type bar = value;
        return bar;
    }
    else
    {
        return_type bar2 = val2;
        return bar2;
    }
}

这不会得到好评。 NRVO仍有可能启动,但它的可能性越来越小。 如果可能的话,构造一个return_value并在不同的代码路径中调整它,而不是返回完全不同的代码路径。

如果NRVO是可能的,这将消除任何开销; 就好像它是直接在调用函数中构造的一样。

如果两种形式的返回值都不可能,则可以进行Move返回

C ++ 11和C ++ 03都有可能进行移动语义; 而不是将信息从一个对象复制到另一个对象,移动语义允许一个对象窃取另一个对象的数据,将其设置为某个默认状态。 对于C ++ 03移动语义,你需要boost.move,但这个概念仍然是合理的。

移动返回没有RVO返回的那么快,但它比副本快得多。 对于兼容的C ++ 11编译器,今天有很多,所有STL和STD结构都应该支持移动语义。 您自己的对象可能没有默认的移动构造函数/赋值运算符(MSVC当前没有用户定义类型的默认移动语义操作),但添加移动语义并不难:只需使用复制和交换习惯用法来添加它!

什么是复制和交换习语?

最后,如果你的return_value不支持move并且你的函数对于RVO来说太难了, 你将默认复制语义,这是你朋友说要避免的。

但是,在大量情况下,这不会明显变慢!

对于原始类型,例如float或int或bool,复制是单个赋值或移动; 几乎没有什么可抱怨的; 通过引用传递这些东西没有一个很好的理由肯定会使你的代码变慢,因为引用是内部指针。 对于像你的bool例子这样的东西,没有理由浪费时间或精力通过参考bool; 返回它是最快的方式。

当你返回一个适合寄存器的东西时,它通常会在寄存器中返回,正是出于这个原因; 它很快,如上所述,最容易维护。

如果您的类型是POD类型,例如简单的结构,则通常可以通过快速调用机制通过寄存器传递,或者优化为直接赋值。

如果你的类型是一个庞大而强大的类型,例如std :: string或其后面有大量数据的东西,需要大量的深拷贝,并且你的代码足够复杂以至于不太可能使RVO,那么可能通过引用传递一个更好的主意。

摘要

  1. 应按值返回任何类型的匿名(rvalue)值
  2. 应按值返回小型或原始类型。
  3. 任何支持移动语义的类型(STL,STD等)都应该按值返回
  4. 应该通过值返回易于推理的命名(左值)值
  5. 复杂功能中的大数据类型应通过引用进行分析或传递

如果您使用的是C ++ 11,请尽可能按值返回。 它更清晰,更快。

这个问题没有一个单一的答案,但正如你已经说过的那样,核心部分是:它取决于。

显然,对于简单类型,例如int或bools,返回值通常是首选解决方案。 它更容易编写,也更不容易出错(因为你不能将未定义的东西传递给函数,并且你不需要在调用指令之前单独定义变量)。 对于复杂类型(例如集合),可能首选call-by-reference,因为它可以避免额外的复制步骤。 但是你也可以返回一个vector<int>*而不仅仅是一个vector<int> ,它会归档相同的(为了一些额外的内存管理的成本)。 然而,所有这些还取决于所使用的语言。 上述内容大多适用于C或C ++,但对于托管类(如Java或C#),大多数复杂类型无论如何都是引用类型,因此返回向量不涉及任何复制。

当然,在某些情况下,您确实希望复制发生,即如果您希望以调用者无法修改被调用类的内部数据结构的方式返回内部向量的(副本)。

再说一次:这取决于。

这是方法和功能之间的区别。

方法(aka子程序)被称为主要调用它们的副作用,即修改作为参数传递给它的一个或多个对象。 在支持OOP的语言中,要修改的对象通常作为this / self参数隐式传递。

另一方面,函数主要被称为返回值,它计算新的东西,不应该修改参数,应该避免副作用。 在函数编程意义上,函数应该是纯粹的。

如果函数/方法用于创建新对象(即工厂),则应返回该对象。 如果传入对变量的引用,那么不清楚谁将负责清理以前包含在变量,调用者或工厂中的对象? 使用工厂功能 ,很明显调用者负责确保清除前一个对象; 使用工厂方法 ,它不是那么清楚,因为工厂可以进行清理,尽管由于各种原因这通常是一个坏主意。

如果一个函数/方法是为了修改一个或多个对象,那么对象应该作为参数传入,不应该返回已修改的对象(例如,如果你'重新设计用于支持它们的语言的流畅接口/方法链接。

如果您的对象是不可变的,那么您应该始终使用函数,因为不可变对象上的每个操作都必须创建新对象。

添加两个向量应该是一个函数(使用返回值),因为返回值是一个新向量。 如果要向现有向量添加另一个向量,那么这应该是一种方法,因为您正在修改现有向量而不是分配新向量。

在不支持异常的语言中,返回值通常用于表示错误值; 但是对于支持异常的语言,错误条件应始终用异常信号通知,并且永远不应该有返回值的方法或修改其参数的函数。 换句话说,不要做副作用并在同一个函数/方法中返回一个值。

什么应该由函数返回,什么不应该(或尽量避免)? 这取决于你的方法应该做什么。

当您的方法修改列表或返回新数据时,您应该使用返回值。 理解你的代码比使用ref参数更好。

返回值的另一个好处是使用方法链的能力。

您可以编写这样的代码,将list参数从一个方法传递到另一个方法:

method1(list).method2(list)...

如前所述,没有一般性答案。 但是没有人谈过机器级别,所以我会这样做并尝试一些例子。

对于适合寄存器的操作数,答案是显而易见的。 我见过的每个编译器都会使用寄存器来返回值(即使它是一个struct)。 这和你一样高效。

所以剩下的问题是大型操作数。

此时,由编译器决定。 确实有些(特别是较旧的)编译器会发出一个副本来实现一个大于寄存器的值的返回。 但这是黑暗时代的技术。

现代编译器 - 主要是因为RAM现在变得更大,而且生活更美好 - 并不是那么愚蠢。 当他们在函数体中看到“ return foo; ”并且foo不适合寄存器时,它们将foo标记为对内存的引用。 这是调用者为保存返回值而分配的内存。 因此,编译器最终生成的代码几乎与您自己传递返回值的引用完全相同

我们来验证一下。 这是一个简单的程序。

struct Big {
  int a[10000];
};

Big process(int n, int c)
{
  Big big;
  for (int i = 0; i < 10000; i++)
    big.a[i] = n + i;
  return big;
}

void process(int n, int c, Big& big)
{
  for (int i = 0; i < 10000; i++)
    big.a[i] = n + i;
}

现在我将在MacBook上使用XCode编译器进行编译。 这是return版本的相关输出:

    xorl    %eax, %eax
    .align  4, 0x90
LBB0_1:                                 ## =>This Inner Loop Header: Depth=1
    leal    (%rsi,%rax), %ecx
    movl    %ecx, (%rdi,%rax,4)
    incq    %rax
    cmpl    $10000, %eax            ## imm = 0x2710
    jne     LBB0_1
## BB#2:
    movq    %rdi, %rax
    popq    %rbp
    ret

并为参考版本:

    xorl    %eax, %eax
    .align  4, 0x90
LBB1_1:                                 ## =>This Inner Loop Header: Depth=1
    leal    (%rdi,%rax), %ecx
    movl    %ecx, (%rdx,%rax,4)
    incq    %rax
    cmpl    $10000, %eax            ## imm = 0x2710
    jne     LBB1_1
## BB#2:
    popq    %rbp
    ret

即使您没有阅读汇编语言代码,也可以看到相似性。 也许有一条指令的区别。 这是-O1 优化关闭后,代码更长,但仍然几乎完全相同。 使用gcc 4.2版,结果非常相似。

所以你应该告诉你的朋友“不”。 使用带有现代编译器的返回值没有任何惩罚。

对我来说,传递一个非常量指针意味着两件事:

  • 参数可以就地更改(您可以将指针传递给struct成员并避免赋值);
  • 如果传递null则不需要返回参数。

后者可以允许避免计算其输出值的整个可能昂贵的代码分支,因为无论如何都不需要它。

我认为这是一种优化 ,即在衡量或至少估计绩效影响时所做的事情。 否则,我更喜欢尽可能不可变的数据,并尽可能使用纯函数,以简化程序流程的正确推理。

通常正确性胜过性能,所以我会保持(const)输入参数和返回结构的明确分离,除非它明显或可证明地妨碍性能或代码可读性。

(免责声明:我通常不用C语写。)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM