函式 (Functions) ------------------------ .. _output-parameters: 輸出用參數 (Output Parameters) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. tip:: 儘量使用回傳值,不要透過參數輸出。如果有僅用來輸出的參數,應該要排在輸入用的參數後面。 C++ 的函式的執行結果一般來說都是透過回傳值,少數情況下會透過參數輸出。 儘量透過回傳值輸出結果,少用參數輸出。一來可讀性較高,二來通常執行效率也較佳。 函式的參數可能做為輸入使用、可能做為輸出使用,也可能同時具有輸入與輸出功能。用來輸入的參數通常是純值或是 ``const`` reference,而輸出(或同時擁有兩種功能)的參數則是指向非 ``const`` 物件的指標。 當我們在決定函式的參數順序時,把「只用來輸入」的參數放在「有輸出功能」的參數之前。特別要注意是:不要把新增的參數放在最後面,只因為它們是最後加入的;把新增的輸入用參數放在輸出用參數之前。 這不是一條無法打破的規則。同時具有輸入與輸出功能的參數(型別通常是類別/結構)常會讓情況變得混亂,而一如我們的大原則,若是為了和相關的函式保持一致,有時你得稍微屈服。 .. _write-short-functions: 保持函式簡短 (Write Short Functions) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. tip:: 儘量保持函式簡短、專注在單一任務上。 我們了解有時長一點的函式是合理的,所以我們並沒有硬性規定函式的長度。如果一個函式的長度超過 40 行,在不破壞程式架構的前題下,可以考慮把它拆成小一點的函式。 即使你的長函式現在運作正常,幾個月後可能會有人去修改、加入新行為。這可能會導致很難找出來的 bug。保持函式簡短可以讓其他人更容易理解、修改你的程式碼。 當你在處理其他人寫的程式碼時,你可能會看到又長又複雜的函式。不要不敢修改現有的程式碼:如果你覺得這樣的函式使用起來很麻煩、很難除錯,或是你想要在不同的情境下使用其中的一小段程式碼,你可以考慮把函式拆成較小且較容易維護的片段。 .. _reference-arguments: Reference 引數 ~~~~~~~~~~~~~~~~~~~~~~~~ .. tip:: 所有以 lvalue reference 方式傳遞的參數都必須加上 ``const``。 定義: 在 C 語言中,如果函式需要去修改某個變數的話,那麼該參數的型別就必須要是指標,例如 ``int foo(int *pval)``。在 C++ 中另外有一種方法是宣告 reference 參數:``int foo(int &val)``。 優點: 將參數定義為 reference 可以避免像 ``(*pval)++`` 這樣不美觀的程式碼。某些應用下是必須的,像是 copy constructors。和使用指標不同,使用時可以清楚確認不會碰到 null 指標。 缺點: References 有時會讓人困惑,因為語法上像是純值,但語意上接近指標。 結論: 在函式的參數列表中,所有的 references 都必須是 ``const``: .. code-block:: c++ void Foo(const string &in, string *out); 事實上,在 Google 的程式碼中有非常嚴格的慣例:輸入用的引數應為純數或 ``const`` references,而輸出用的引數則為指標。有時候輸入用的參數會是 ``const`` 指標,但我們絕不允許使用非 ``const`` reference 當成參數,除非有其他的慣例需求,例如 ``swap()``。 然而,有些時候輸入用參數的型別訂為 ``const T*`` 會比 ``const T&`` 來得恰當。舉例來說: - 參數有可能是 null 指標。 - 函式會儲存輸入物件的指標或 reference。 請牢記:大部份的情況下,輸入用參數都必須指定為 ``const T&``。使用 ``const T*`` 會傳達給閱讀程式碼的人不太一樣的意義。所以若是你選擇了 ``const T*`` 而非 ``const T&``,你得有足夠的理由;否則,閱讀程式碼的人可能會以為有什麼特別的意圖,試著去尋找不存在的解釋,徒增無謂的困擾。 .. _function-overloading: 函式多載化 (Function Overloading) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. tip:: 使用多載化函式(包括建構式)的條件是:必須能讓閱讀程式的人只要看呼叫端的程式碼就可以大致知道會發生什麼事,而不需要先去猜到底哪個多載化實作會被呼叫。 定義: 你可以寫一個接受 ``const string&`` 參數的函式,然後再寫一個接受 ``const char*`` 的多載化版本。雖然以這個例子來說,你應該要考慮使用 ``std::string_view``。 .. code-block:: c++ class MyClass { public: void Analyze(const string &text); void Analyze(const char *text, size_t textlen); }; 優點: 多載化讓程式碼看起來更直覺,因為相同名稱的函式可以接受不同的參數。在某些使用模板的程式碼中可能是必要的,而且對於 Visitor 類別來說很方便。 根據 ``const`` 或是 ``&`` 屬性產生的多載化可以讓工具程式碼更方便使用、更有效率,甚至兩者兼具。(`TotW 148 `__ 中有更多詳細的說明。) 缺點: 如果函式僅靠引數的型別多載化,閱讀程式者可能得要了解 C++ 複雜的比對規則,才能知道發生什麼事。此外,在類別繼承的時候,若是衍生類別只改寫 (override) 了多載化函式的其中幾個版本,這樣的語意會讓許多人感到困惑。 結論: 我們允許函式多載化,只要組多載化出來的函式在語意上是一致的即可。可以依據不同的型別、參數屬性,或是參數個數進行多載化。閱讀程式者必須要能「不需要」知道哪個版本的函式會被喚起,只需要知道這組函式裡的 **有些東西** 被喚起。如果你在標頭檔裡只寫一段註解,就幫可以完整說明整組的多載化函式的功能,那就表示這組多載化函式設計得宜。 .. _default-arguments: 預設引數 (Default Arguments) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. tip:: 你可以為非虛擬函式加上預設引數,只要這個預設值的實值內容保證不會變。遵守和 :ref:`多載化函式 ` 一樣的規則。如果使用預設引數所能增加的可讀性無法蓋過下面討論的缺點,建議儘量使用多載化函式。 優點: 你常會遇到一個函式會使用某項預設值、但偶爾需要改用其他數值的情況。利用預設參數你可以利用簡單的方法避免為少數情況定義新的函式。和函式多載化比起來,預設引數的語法較簡潔,不需要寫很多重覆的程式碼,而且「必要」和「選用」的引數也分得很清楚。 缺點: 預設引數其實就是達成多載化函式的另一種方法,所以 :ref:`使用上的限制都和多載化函式相同 `。 呼叫虛擬函式時的引數預設值,會依目標物件的靜態型別 (static type) 而定;但你沒有辦法保證所有改寫這個函式的人都給它一樣的預設值。 預設參數在每個呼叫處都會被重新計算,會增加執行碼的大小。閱讀程式者也會認為預設值在宣告時就固定下來了,而不是每次呼叫時都會重新計算一次。 若是預設引數中包括函式指標的話也會讓人困擾,因為函式的 signature 和呼叫的 signature 常會不同。使用函式多載化可以避免這樣的問題。 結論: 虛擬函式禁止使用預設引數,因為它們的行為不符合預期。另外,也不要讓預設值的計算在不同的地方呼叫會有不同的結果(例如:不要寫出類似 ``void f(int n = counter++);`` 這樣的程式碼)。 在某些情況下,預設引數可以增加函式的可讀性,且得到的好處大於上述的壞處,此時可以用預設引數。如果你不能確定,那就改用多載化。 .. _trailing-return-type-syntax: 後置式回傳值型別語法 (Trailing Return Type Syntax) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. tip:: 只有在原本的(前置式)回傳值型別語法無法使用或可讀性真的很低時,才使用後置式回傳型別。 定義: C++ 定義了兩種不同的函式宣告語法。原有的語法是將回傳值的型別放在函式的名稱前面。例如: .. code-block:: c++ int foo(int x); 從 C++11 開始加入了新的語法:在函式名稱前面加上 ``auto`` 關鍵字,然後在引數列表之後加上後置式的回傳值型別。舉例來說,前面的函式宣告可以寫成下面的形式: .. code-block:: c++ auto foo(int x) -> int; 後置式的回傳值型別存在於函式作用域中。對於簡單的型別(像是 ``int``)來說,這不會有什麼不同;但面對更複雜的情況時就有差,像是在類別作用域中宣告的型別,或是依函式參數而定的型別。 優點: 後置式回傳值型別是用來明確指定 :ref:`lambda 運算式 ` 回傳值的唯一方法。在某些條件下編譯器可以自行推斷 lambda 的回傳值型別,但還是有推不出來的情況。即使編譯器可以自動推斷,有時候明確指定對程式碼閱讀者來說還是比較清楚的。 有時候把回傳值的型別放在函式的參數列表之後,對程式碼閱讀者來說比較清楚。特別是當回傳值的型別依據模板參數而定時。舉例來說: .. code-block:: c++ template auto add(T t, U u) -> decltype(t + u); 對照原有語法: .. code-block:: c++ template decltype(declval() + declval()) add(T t, U u); 缺點: 後置式回傳型別相對來說是很新的語法,而且其他和 C++ 很像的語言(例如 C 和 Java)都沒有類似的語法,因此對某些閱讀程式者來說可能無法理解。 現存的程式碼中,已經有大量的函式宣告採用原本的語法,想要全部換成新語法不太可能,因此比較實際的作法是繼續選用原本的語法,或是混用兩種語法。選用其中一種語法對於風格的統一來說比較理想。 結論: 在大部份的情況下,繼續使用原有的前置式回傳值型別語法宣告函式。只有在非用不可時(像是 labmdas),或是將回傳值的型別放在參數列表之後可以大幅增加可讀性的情況下,才使用新的語法。後者發生機會不高;只有在撰寫非常複雜的模板程式時才會遇到,而在大部份的情況下,我們 :ref:`不鼓勵撰寫這樣的模板程式 `。