Minimizing Function Scope
Posted on May 13th, 2007 at 6:45 by fr3@K

Intro

C++ 函數可以根據用途放在 (宣告/定義) 不同的 scope. 一個 function 可以是 free function, member function 或是 static member function. 它們不但可以被放在指定的 namespace 裡面. 後兩者更可被賦予 public, protected 或 private 的存取控制 (access control). 這篇要談的不是這些 function 的不同, 更不是三種存取控制的差異. 而是要談如何決定一個 function 該被宣告在哪裡.

過度使用 C++ 的 language feature 可說是在 C++ 程式中常見到的毛病. 即便是有數年經驗的程式設計師所寫出來的程式也會有這樣的問題. 其中, 最容易犯下的, 也是最難抗拒的誘惑之一是 – 沒有充分的理由, 就把 function 擺在不恰當的 scope (通常為會導致更多 coupling 的 scope). 譬如說, 把因為某個 class 而寫的 function 宣告為該 class 的 (static) member function.


Scenario

假設我們設計了個 class, 用以解析以 key=value 格式組成的設定檔. 以下為這個 class 提供給使用者的介面 (本篇將以忽略 const correctness, 並使用 pass-by-value 等方式簡化 example code, 以利說明重點):

// 項目: key=value
// 索引: key
// 值: value
// conf_parser.hxx
class conf_parser
{
public:
    // 打開並解析一個設定檔
    conf_parser(string filename);
    // 測試設定檔中是否有以 key 為索引的項目
    bool has_item(string key);
    // 取出以 key 為索引項目對應的值
    string get_value(string key);
};

在實做過程中, 我們發現需要個用來把項目中的無效字元 (假設只接受英文字母, 數字與等於符號) 去除的 function, 以利接下來把項目拆解為索引與值兩部份. 於是加了個 member function:

// conf_parser.hxx
class conf_parser
{
public:
    conf_parser(string filename);
    bool has_item(string key);
    string get_value(string key);

    // 去除從檔案讀出的一行 (也就是一個項目) 中的無效字元
    string remove_invalid_characters(string item)
};

// conf_parser.cxx
string conf_parser::remove_invalid_characters(string item)
{
    string result;
    for(string::const_iterator iter = item.begin();
        iter != item.end();
        ++iter)
    {
        char ch = *iter;
        if(isalnum(ch) ||
           ch == '=')
        {
            result.push_back(ch);
        }
    }
    return result;
}

試問, 以上這個 function 的宣告有沒有問題? 被擺在正確的 scope 了嗎? 或是該被放在哪個 access control (public/protected/private) 層級? 還是有其他更好的選項? 接下來讓我們一步一步分析.

Analysis

首先, 這個 function 是 class conf_parser 內部使用的一個 helper function, 因此不該被放在 public.

那 protected 呢? 也不對. 除了特例之外, 被放在 protected 的 member 的語意 (semantics) 是 – 該 member 是提供給子類 (derived class) 的介面. 我們可以從 class conf_parser 的清楚規範的功能與未宣告 virtual destructor 的 class definition 明確看出, class conf_parser 不會被繼承, 或是說不打算也不願意被繼承.

好, 那放在 private 可以了吧!? 還是不對. 其實 remove_invalid_characters 根本不該是個 member function. 在其 function 的 body 內, 沒有參考到任何其他 class member, 就是它不應當是 member function 最好的理由.

你說, 那它應該要是個 static member function. 如果看出這一點的話, 恭喜你, 今天的討論的題目你已經及格了. 如果還看出這個 static member function 應該要是 private, 那更有八十分.

的確, 函數 remove_invalid_characters 可能適合成為一個 private static member function. 最後一個決定它該不該是個 static member function 的條件, 在於有沒有其他的 inline member function 參考到它. 如果有其他 inline member function 參考到它, 它就必須能被參考它的 inline member function 看見. 也就是說它必須要宣告在 header file. 這個時候, 由於它是 class conf_parser 內部使用的 helper function, 將它宣告為 private static member function 不但可以精簡 class conf_parser 的介面, 更可以確保使用者不會參考到它.

既然把 remove_invalid_characters 宣告為 private static member function 已經讓 class conf_parser 的介面精簡了, 也讓使用者不致誤用. 為什麼只有八十分? 到底要怎麼做才能拿到九十分以上? 答案是把它的宣告從 header 中徹底移除, 放到 source file 裡面:

// conf_parser.hxx
class conf_parser
{
public:
    conf_parser(string filename);
    bool has_item(string key);
    string get_value(string key);
};

// conf_parser.cxx
// 去除從檔案讀出的一行 (也就是一個項目) 中的無效字元
/*unnamed*/ namespace
{
    string remove_invalid_characters(string item)
    {
        string result;
        for(string::const_iterator iter = item.begin();
            iter != item.end();
            ++iter)
        {
            char ch = *iter;
            if(isalnum(ch) ||
               ch == '=')
            {
                result.push_back(ch);
            }
        }
        return result;
    }
} // unnamed namespace

在上面的例子裡面, 把 remove_invalid_characters 宣告為在 source file 的 unnamed namespace 中的 free function 帶來的好處其實有限. 說穿了就只是能讓 header file 看起來更簡潔. 雖然說這對閱讀我們的 header file 的人有明顯的好處, 但在此例中對 compiler 以及使用它的程式來說, 幾乎沒有任何差異. 然而, 在適當的時候, 把 function 宣告為 (在 source file 的 unnamed namespace 的) free function 依然是個值得鼓勵的好習慣.

想像一下, 如果 class conf_parser 的 header file 本來沒有用到 class string, 只因為 remove_invalid_characters 的參數用到了 class string 而需要把定義 class string 的 header include 進來, 或至少 forward declare class string. 再想像一下, 假如 remove_invalid_characters 的參數用到了一個只有 class conf_parser 的 source file 才會使用到的 user defined type. 如果把 remove_invalid_characters 宣告在 header file, 就迫使定義被使用到的 user defined type 的 header 必須被 include 或是定義 (或至少 forward declare) 在 conf_parser.hxx. 又在無形中增加了不必要的相依性.

接下來我要問一個比較有趣, 也希望能讓你思考一下的問題. 假設, 我們的設定檔能接受的字元從只是數字, 英文字母與等號, 變成除了連續的 whitespace 之外的所有字元都能接受. 你會把我們的 helper function (把一個或連續的 whitespace 替換成一個 space) – remove_consecutive_whitespace 宣告在哪裡?

因為這個規格的改變, 我們的 helper function 所做的事情將從為了 class conf_parser 量身訂做, 轉變為一個更通用 (generic) 的功能. 它不但能被用在解析我們設定檔的前處理 (pre-processing), 也能用在其他地方, 譬如計算字數 (word count) 的前處理. 這時候這個 helper function 就不該躲 (宣告) 在 conf_parser.cxx 這個 source file 裡面, 應該放在與 class conf_parser 沒直接關聯, 但其他人也能參考到的地方. 例如宣告在一個叫做 utility.hxx 的 header file.

Conclusion

Function 應該要被放在相依性最小的 scope 裡面. 相依性由小而大的 scope 排列如下 unnamed namespace in source file < free function (possibly in namespace) < static member function < member function. 最後提供幾項在許多情況下可以用來參考的基本原則:

參考到某 class 的 private member 為該 class 的公開介面之一 Reusable 被該 class 的 inline member function 參考到 宣告的 scope
No No No No Source file only (unnamed namespace)
No No Yes - Free function
No No No Yes Private static member function
No Yes - - Public static member function
Yes No - - Private member function
Yes Yes - - Public member function

del.icio.us:Minimizing Function Scope digg:Minimizing Function Scope spurl:Minimizing Function Scope newsvine:Minimizing Function Scope furl:Minimizing Function Scope Y!:Minimizing Function Scope 黑米共享書籤:Minimizing Function Scope 推推王:Minimizing Function Scope
Previous Post
« Copyright 2007 fr3@K «
Next Post
» Happy Birthday, My Dearest Brother »

12 Comments »

Comment #2876

方法不錯,不過看來只有在 C++ 可以拿 90 分,反觀 Java, PHP5 的語言反而只能用 Static function 就只能有 80 分了

話說回來,使用這方法,如果這檔案常常被改,反而要多一些 Documentation 因為這不是 OOP 的功能。不過在 header 內成為 private static function 或 private object function 又有什麼關係呢?因為是 Static linking 所以在執行上只要不用到也不會慢,只要在 Comment 多注意一些也不失為一個好方法

Comment by Bam — May 18, 2007 @ 4:46


Comment #2877

放在 utility.hxx 是一個很好主意,雖然每個人都有自己的 utility.hxx 。數量上較多,但最後都可合而為一加而簡化

Comment by Bam — May 18, 2007 @ 5:00


Comment #2878

>class conf_parser 的 header file 本來沒有用到 class string, 只因為 >remove_invalid_characters 的參數用到了 class string 而需要把定義 class string >的 header include 進來,又在無形中增加了不必要的相依性.

多多使用 Generic class 增加相依性.也有一些好處
1. 使用上明確,學習上沒有困難
2. 如果沒有用到,也不會使程式加大,而使速度變慢 (使用 Static linking)
3. 使用 OOP 的方法,規式明確
4. 因為是使用 Generic class 反而是一個較好標準,不需修改,自製的功能相對較常變動….

Comment by Bam — May 18, 2007 @ 5:12


Comment #2888

方法不錯,不過看來只有在 C++ 可以拿 90 分,反觀 Java, PHP5 的語言反而只能用 Static function 就只能有 80 分了

除了 C/C++, 我對其他語言涉獵極為有限, 在這裡談的 coding 相關題材, 幾乎都是 C++ specific.

話說回來,使用這方法,如果這檔案常常被改,反而要多一些 Documentation 因為這不是 OOP 的功能。不過在 header 內成為 private static function 或 private object function 又有什麼關係呢?因為是 Static linking 所以在執行上只要不用到也不會慢,只要在 Comment 多注意一些也不失為一個好方法

你這段沒說清楚, 我只好自己猜了. 我猜你說的情形是把 remove_invalid_characters 移到 conf_parser.cxx 變成 free function in unnamed namespace 接下來發生的狀況.

把任何東西從 header 移到 source file 幾乎都可以降低因為內部的改變而造成對外的影響. 前面提到的移動也不例外. 不移, 保留為一個 static private member function 也沒有特別的不好. 移動後, 即使我對 remove_invalid_characters 做了更動, 我的 client 不會需要重新 compile 他們的 code. 在我的看法裡, 那就是八十分與九十分的差異.

說真的, 除了資料封裝, 我在實務上並不常使用所謂正規的 OOP 方法 (如 inheritance, dynamic polymorphism 等), 自然個人興趣不大, 談的也少. 這篇談的與 OOP 或是 (static) linking 無關. 這裡說的相依性是一個 function 與其他有互動關係的 code 之間的相依性.

多多使用 Generic class 增加相依性.也有一些好處
1. 使用上明確,學習上沒有困難
2. 如果沒有用到,也不會使程式加大,而使速度變慢 (使用 Static linking)
3. 使用 OOP 的方法,規式明確
4. 因為是使用 Generic class 反而是一個較好標準,不需修改,自製的功能相對較常變動….

沒看懂這段呢. 多做點說明如何?

你可以使用 <blockquote> 來引述我或是其他人的文字. 比較不會因為字體大小的改變而造成排版問題.

Comment by fr3@K — May 20, 2007 @ 3:09


Comment #2915

我投 free function 一票,以前我也是把 helper function 放在class 中,到後來覺得實在受不了… 根本不是該class所需要提供給user用的helper function 全部放在class 當作 member function ,真是太亂了。所以我就試著用 free function 來做看看,ㄚ~~ 果然輕爽多了。而且這些動作本來就不是要提供給user 去call的,這樣我這 class 看起來目標就很明確不會有雜七雜八的member function 混在裡頭摸魚打混的,看了心就煩。後來看到Effective C++ 3/e 也有提到這樣做是比較好的,因此更加深我對free function的認同。
沒錯當我第一次見到 template 後就漸漸打破 正規的 OOP 方法,所以混合也許比純種好。java 就不能體會到C++ template的威力與好處了。
template 給我的感覺就不是正規OOP了,所以無招勝有招,無形勝有形就是我的體會了。任何方法都可能是好辦法,不需拘泥於某種形式。像就有人提出不要使用巨集但是有些人就是可以把他發揮到淋漓盡致讓程式看起來很優雅,所以巨集何嘗不是好東西呢。

Comment by 烤布雷 — May 24, 2007 @ 1:31


Comment #2916

烤布雷,

好個 `無招勝有招,無形勝有形’ , 改天教教我你的蛇行刁手吧~ :P

C++ templates 的確與 OOP 無關. Templates 是 C++ 實現 meta programming 的手段.

Comment by fr3@K — May 24, 2007 @ 10:23


Comment #2928

了解,謝謝 fr3@k 的細心說明
之前沒有選擇 free function 是因為對 free function 對 inheritance, dynamic polymorphism 等等 有較多的不便。使用 static function 在放置於其它的 class 也可以增加相關性。如類於 remove_invalid_character, 的 remove_leading_white_space function 等等,可放在同一個 namespace /or class 內增加活用性。

“不移, 保留為一個 static private member function 也沒有特別的不好. 移動後, 即使我對 remove_invalid_characters 做了更動, 我的 client 不會需要重新 compile 他們的 code. 在我的看法裡, 那就是八十分與九十分的差異.”

ed: proper quoting – fr3@K

因為是 member(or static class) function 只要 function signature 沒有改變 (如 name, arguments) ,基本上是不用重新 compile 的,如果改了,那也只好 recompile 全部的相連文件. 也因為是 “private” 的,client 使用到的可能性為0 除了正在寫程式的自己

我只能說這像的寫法是在寫給別人看的,把所有的東西寫在一處,分類再慢慢注解。

對於像 remove_invalid_characters 之類的小東西,為了自己的懶和寫程式的速度,在下也是未放置於 .hpp 內,直接於 .cpp 內宣告和使用,又或是另建一個小小的 uility.hpp 和 ultilty.cpp 供別人 reuse。較前的方法由於未放置於 header,因此無法在同一處找可自己可 reuse 的 function 有時也忘了自己是否寫過,只能付之於較好的 IDE 想辨法更快的在程式海內翻找。這點亦和傳統的 C 處理方式相同

和 烤布雷 說的一樣,無招勝有招,其實次常見的 helper function 通常用於 recursive function 這個就常定義於 header 內,因為通常 recursive, 等等都是些難題使用的簡化後通用版本,對於 inheritance 來說,這些比起容易的 remove_invalid_characters 較可能幫助 subclasses

Comment by Bam — May 26, 2007 @ 15:12


Comment #2929

不好意思,又亂掉了…. 簡單的就是說,如果只寫把 helper functions 寫於目前 .cpp 只能代表未來也會如此下去。這次 remove_invalid_char 下次 remove_leading_white_space.. 等等,… 一直繼續下去

換了一個檔開始寫也許又要 剪貼 一次,每個程式 2000 行可能 400 行都是 剪貼 來的,每個程式檔都剪貼一次也是大工程,這個 remove_invalid_char 在這個 abc.cpp 檔和那個 remove_invalid_char def.cpp 檔一樣也難分辨,這才是我反對的主因

那不如就建個 .h, 直接 #include 下去 有分類的話更好,新的就 #include 擁有個好名字
如果必須要 recompile 的話也只是目前的檔,如果 .h 有修改的話也只有新增,更可換個新檔名,這樣舊程式可以不用 recompile. 使用的版本也知道

如 ultility11.h ( version 1.1) 等等

Comment by Bam — May 26, 2007 @ 15:26


Comment #2930

有沒有 OOP 都沒有關係。 有在 .h 宣告的 free function 和沒有在 .h 宣告的都一樣,怕的只是有同名的引用問題,這在以前的 C 是無解

好在 c++ 內有 namespace 這些 free function 都可以在 .h 內給他們一個 namespace, OOP 只是另一個給他們 namespace 的方法而己

function 都要寫成通用的,不然就直接寫上不用 function 也比多使用 function 來的容易,通常可寫成 function 的東西都可以寫常通用的吧, 利外就是如我所說的 recursive helper function 重複可能性差不多是0,就寫在 .cpp 內吧,別的 .cpp 也不可能用同樣的東西。所以文章內的 remove_invalid_char 可能不是一個好例子,別的 .cpp 也可能有同樣的 invalid char 所以也有可能用同一個 function.

有錯誤請指教 謝謝,不意思寫了那麼多

Comment by Bam — May 26, 2007 @ 15:40


Comment #2936

之前沒有選擇 free function 是因為對 free function 對 inheritance, dynamic polymorphism 等等 有較多的不便。

別忘了 class/struct (even C++ in general) 能做的不只是 inheritance 與 dynamic polymorphism 這些東西. 以我來做例子的話, 我寫的十個 C++ class 中, 可能還不到一個有有意義的 inheritance 或 dynamic polymorphism (為了偷懶才用的 inheritance 不算 :P ). 請參考我前兩天寫的 Avoid Pointer Parameters and Inheritance, 裡面有輕微的觸碰到 inheritance 之惡這個議題.

=====================================

如類於 remove_invalid_character, 的 remove_leading_white_space function 等等,可放在同一個 namespace /or class 內增加活用性。

這篇所假設的 `無效字元’, 是 class conf_parser 開出來的規格. remove_invalid_character 是量身訂做的 function, 自然不需要 `增加活用性’ 的機會. 倒數第二段的文字說得最清楚:

因為這個規格的改變, 我們的 helper function 所做的事情將從為了 class conf_parser 量身訂做, 轉變為一個更通用 (generic) 的功能.

=====================================

因為是 member(or static class) function 只要 function signature 沒有改變 (如 name, arguments) ,基本上是不用重新 compile 的

是的, 在我舉的例子下, 你說的是對的, 讓我更進一步說明. 不知道你清不清楚 pimpl 這個 idiom (慣用法). 我試著在網路上找好的說明但… 失敗了. 如不清楚請參考 Exceptional C++ 的第一百五十三頁. 把 helper function 宣告在 source file (裡的 unnamed namespace) 與 pimpl idiom 的手法當然不同, 但目的一樣 – 在可能的情況下降低相依性.

=====================================

由於未放置於 header,因此無法在同一處找可自己可 reuse 的 function 有時也忘了自己是否寫過,只能付之於較好的 IDE 想辨法更快的在程式海內翻找。

忘了自己寫過… 大概是無解的 XD. 我的觀點主要是就寫 library 的人而言. 這時文件就顯得格外重要. 另外, 我非常不建議依賴 IDE.

=====================================

這點亦和傳統的 C 處理方式相同

其實次常見的 helper function 通常用於 recursive function 這個就常定義於 header 內,因為通常 recursive, 等等都是些難題使用的簡化後通用版本,對於 inheritance 來說,這些比起容易的 remove_invalid_characters 較可能幫助 subclasses

不解…

=====================================

對於 OOP 來說,當然是相依性愈多愈好,但是要把他們分類,如常用汽車工具,機車工具

這點我無法認同. 一個 class 的相依性愈強, 就表示即使只需要重複使用其中的一部分 (如一個 helper function), 就必須得概括承受其所有的相依性. 我的重點之一是說, 不會被重複使用的東西就要盡量隱藏起來.

我想你可能是掉進 `能用 C++ 的 language feature 就盡量用’ 的陷阱. Exceptional C++ 是一本好書 (有中譯本), 請儘快找一本來拜讀.

=====================================

1.
就是我一開始所說的忘記問題 或 為了未來,可放置於不同的 .h/cpp 的分類,而暫時放於目前的 .h 內,等待之後的 clean up
我覺得有自己的 utility 或為了每個 project 建立專用的 utility 是有必要性的
放置太多的 function 於 free function 於目前的 .cpp 內而沒有放於專用的 little_utility.h
只會同一個程式碼在數個檔出現的情形,只能說是應付目前的因局的快速方案

與 project 相關, 先哪裡方便就哪裡放以待後面 cleanup, 我想是沒大問題. 可以你的心態要改變一下, 對於一個好的 library writer (也可以放到任何一個 programmer 身上) 來說, 沒有所謂的 `自己的 utility’. 寫可被 reuse 的 code 的時候應該是要功能導向, 如 string utility (library), network utility (library). 應該以功能分類, 甚至是放到不同的 library 裡面. 寫的時候不該以 – 這是我的 utility – 的心態來做. 要也能有效幫助別人解決問題才是.

=====================================

2.
我覺得如果自己的 mystringUtility.h/cpp 有 2000 個 function也是一個美事,
因為是比較通用的東西,被改的機會真的不大,且增加 function 並不會造成 recompile
沒有用到的 function 有沒有刪除也沒差, 對 link 都沒有分別

我相信你舉 2000 這個數字只是為了說明 `很多’. 否則通常 2000 個 function 在同一個 header 不會是好事. 我就以很多 utility function 放在同一個 header 回應.

你對自己太有自信了, 我不相信任何一個人能確認自己或他人寫的許多 class/function 沒問題. 我這裡說的問題不是實做上的問題, 而是介面 (interface) 的問題. 就連 C++ 中的王者 – C++ Standard Library – 一個在發表前已經被無數專家 review 過的 library, 都會在事後被發現有介面上的問題. 將在新版中訂正. 更何況是你我這幾個 programmer?

而介面一但被更改, 結果就是所有依賴這 header 的 compilation unit 都要被重新 compile. 不只是 link 的問題.

=====================================

3.
這個有沒有都一樣,對於 C 的 free function 方法有時,反而比 OOP 還來的快,因為
小 function 沒有 inheritance 的可能性。我只能說 OOP 的對映方法就是 static class function
只是檔名變 class name, compile time 也加長了

不解…

=====================================

4.
這個是分類之後才可能做的到的,在 project 完結時,所有的通用 function 基本上
有了它們放置之處,也有了分類,這時通常就不會在變了,變成一個獨立的 package 可有自己的 namespace
將打包給未來 unknown project 2 使用
最後不管是用什麼方法,最後都是要把常修改的和非”常修改”的東西分開、獨立

參考我前面的回應. 以功能分類, 不要想著這是我的 utility.

=====================================

基本上目標不都是同一種東西 reuse,只是想法不同罷了
沒有 header 也就沒有 reuse 的可能了

參考我前面的回應. 有時就是不該 (不要) 被 reuse, 這時就可以我提出的方法降低相依性.

Comment by fr3@K — May 27, 2007 @ 3:43


Comment #2937

換了一個檔開始寫也許又要 剪貼 一次,每個程式 2000 行可能 400 行都是 剪貼 來的,每個程式檔都剪貼一次也是大工程,這個 remove_invalid_char 在這個 abc.cpp 檔和那個 remove_invalid_char def.cpp 檔一樣也難分辨,這才是我反對的主因

別搞錯, 千萬別把我的建議解讀為鼓勵 copy & paste. 正好相反, 我痛恨 copy & paste. 我唯一一次對上司拍桌子是因為對方在一次提倡 code reuse 的會議裡, 說 copy & paste 是好的 reuse 而且被我指正之後還搬出甚麼以他十幾年經驗… 等等說詞教育我.

建議你好好再看一次這篇的文字. 也許我一開始說得不夠清楚. 經過這幾輪的評論與回應, 希望可以讓你進一步理解我的原意.

Comment by fr3@K — May 27, 2007 @ 4:00


Comment #2938

Bam,

你應該是個小老弟吧!? 先對你說聲抱歉, 雖然原則上我很願意仔細回答你的評論 (尤其是你也寫了這麼多), 但你的問題不但多而且論述雜亂, 讓我很難好好回應你.

給你點小建議:

  • 試著把你的意見表示得更清楚
  • 簡單的 HTML 學好
  • C++(例如 namespace)/OOP/dependency 等等基本上不是同一回事, 不要混為一談才是
  • 你的評論裡面太多 `肯定’ 的主張. 要了解懂得愈多的人更會知道其實自己懂得有限這個道理. 從你的發言裡, 我猜測你大概不是個不知天高地厚的小朋友, 有自信是好的, 但千萬不要 (不要跟我一樣 :P ) 過於自滿
  • 火速去看 Exceptional C++More Exceptional C++ 這兩本書

打這麼多字手都累了, 希望能對你有幫助.

Comment by fr3@K — May 27, 2007 @ 4:14


Comments RSS TrackBack URI

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>