簡體   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