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 |
![]() |
|
| Previous Post « Copyright 2007 fr3@K « |
Next Post » Happy Birthday, My Dearest Brother » |








方法不錯,不過看來只有在 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