[爆卦]函式原型宣告是什麼?優點缺點精華區懶人包

為什麼這篇函式原型宣告鄉民發文收入到精華區:因為在函式原型宣告這個討論話題中,有許多相關的文章在討論,這篇最有參考價值!作者purpose (purpose)看板C_and_CPP標題Fw: [心得] 原型宣告與 nam...


※ [本文轉錄自 Programming 看板 #1Fa607Rc ]

作者: purpose (purpose) 看板: Programming
標題: [心得] 原型宣告與 namespace 之敵:ADL
時間: Fri Apr 20 02:57:09 2012


之前這篇文章
#1FZiIX_c (C_and_CPP) [ptt.cc] [問題] member function與friend function 重複

討論一系列函數原型宣告的議題,最後版主提及 ADL 相關的東西,
看完後有點想法,所以寫篇 ADL 的介紹文章跟大家交流一下。


論原型宣告

在 C 語言中不強制要求原型宣告,對於參數的資料形態檢查也很寬鬆。

C++ 後開始嚴格要求,一方面是 overload 需要名稱先修飾過才能達成,
而名稱修飾要先知道參數的形態。另一方面是實際參數的形態跟
形式參數的形態可以相異,這種種要求導致 C++ 對原型宣告的渴望。

原型宣告只是手段,真正的目的是取得函數的 signature,也就是
函數名稱為何、每個參數的資料型態、順序。

現今三大主流語言 C/C++, JAVA, C# 中,只剩下 C++ 依賴原型宣告,
所以要用 include 把 header 裡的原型宣告全部塞進原始碼。
而其他兩大陣營各有辦法取得 signature,其細節稍後再談,總之
在 JAVA 使用 import,在 C# 則使用 using namespace。

論 namespace

承上可知,在 C++ 裡面的 namespace 其觀念、角色、責任,
相對於 C# namespace 來說,是比較狹窄的。

可能也因為這樣,很多 C++ Style 都建議不要把 C++ namespace 裡
的東西做縮排;而 C# namespace 裡的東西,則一向都有在縮排。


[h2] 為什麼要有 namespace? [/h2]

很多函數名稱很棒,比如 foo,這種好名字大家都搶著用。
即便有 overload 機制存在,還是會出現同名同參數的狀況,導致編譯錯誤。

所以 C++ 引入 namespace 觀念,只要把愛用的名字,比如 foo 放進
自己的 namespace 裡,那該函數就會擁有全名 (fully qualified name)。
其他人使用不同 namespace,所以大家全名不同,就避免掉 ambiguous。
在避免 ambiguous 的同時,又懶得每次都打函數全名,所以 using 出現。

總歸一句,C++ namespace 的功能就單純是:讓全名出現。

[h2] 廣義的 namespace (C# 與 JAVA) [/h2]

C# namespace 則有更多的意義:程式組織的一部分、幫助取得 signature
JAVA 的狀況跟 C# 接近,只是 namespace 變 package。

C# 跟 JAVA 都不像 C++ 是多範式的,他們是純種的物件導向。
就拿最基本的函數、變數來說,因為前兩者是純爺們,所以萬事萬物皆類別,
所以哪怕是再小的函數,那也必須是某類別的一部分。
而 C/C++ 全域函數本身,是不需要強制規屬於某類別的。

C# 類別必須是某 namespace 的一部分,
換句話說,每個 C# 程式的組織中必然要有至少一個 namespace,然後
每個 namespace 裡至少要有一個類別,最後每個類別裡才可以有函數存在。

因此要寫 C# 版的 Hello World 就得這樣搞:
namespace HelloWorld
{
class Hello
{
static void Main()
{
System.Console.WriteLine("Hello World!");
}
}
}
如果省略 namespace HelloWord 那 C# 也會自動把 Hello 類別
加入到預設的 global namespace 裡去。

[h2] namespace 幫助取得 signature? [/h2]

函數的全名之中,包含其所屬的 namespace、class 等資訊,
可以知道該函數是屬於哪個組織的,當 C#、JAVA 編譯器知道該函數的
完整組織時,就能找到 signature。

JAVA 程式組織很單純,一個原始碼檔 MyApp.java 對應一個 MyApp 類別,
編譯後變成一個 MyApp.class 檔案。
(而 C# 一個原始碼檔案 *.cs 裡可以有多個 namespace)
而且一個 JAVA package 又對應一個檔案系統裡的資料夾。

所以知道 JAVA 函數全名,比如叫 MyPack.MyClass.foo 就能知道
在 MyClass.class 檔案裡,可以解讀出 foo 的 signature 資訊。
而這個 MyClass.class 檔有可能位於 MyPack 資料夾裡面,也有可能跟
其他 *.class 檔被合併包裝成一個 jar 檔案。其中會有一個環境變數叫
classpath 裡面的目錄也會是搜尋目標。

使用 foo 函數時,不一定每次都打全名,有可能只寫 MyClass.foo();
所以得先用 using 或 import 使編譯器知道哪些 namespace 下,
可能找得到該類別,順利得知其全名資訊。

至於 C# 的做法是直接開 .exe、.dll 檔,這些由 C# 生成的二進位檔,
有包含中介資訊,所以可以用 ildasm.exe 將其反向得知包含哪些
namespace、class、method,甚至是由 IL 碼撰寫的 method 定義,那取得
一個區區的 signature 自然不在話下。
但具體的查找規則我就不知道了,就假設是窮舉吧。

可以想像成 C# 有能力把成品的 .exe、.dll 反向出原始碼,再從原始碼查找
signature;相對的 C / C++ 即便用最強的 Hex Rays Decompiler 雖然函數
可以被反向成 C 語言,但函數的原始名稱、參數的原始資料形態,
這些都已經遺失掉,無法像 C# 一樣得到精確的資訊。


全域函數 (C/C++ 特產)

不屬於 class 的函數,都是全域函數,只有非純 OO 的語言才能擁有。

就拿運算子多載來說,C# 限定它只能是類別裡的 method,但 C++
可以有兩種寫法,另外一種就是寫成全域函數。

可以想見的是,當某類別的開發不是由你掌管,你也可以自己寫一個
全域版本的運算子多載來擴充,從這角度來看,C++ 在運算子上的
多型能力是比 C# 強悍的。(所以 C# 比較少強調運算子)

當然因為有兩個選擇,如果兩者同時存在,就會發生 ambiguous。

namespace 觀點下的全域函數

namespace test{
void foo() { }
}

要使用時,就打全名呼叫 test::foo();
或者用 using namespace test; 就可以直接使用。

ADL

先講結論: 少用 ADL,如同少用 friend 少加味精一般

ADL 全名是 <STRIKE> 反人類社會極端捉摸不定異常撲朔迷離... </STRIKE>
引數相關名稱查詢 (Argument-Dependent name Lookup)。

簡單來說,每個函數呼叫,原本的全名查詢規則很單純,先看
this->函數名(...) 是否存在,沒有就找全域函數。

有了 ADL 之後,this->函數名(...) 依然最優先選用,
但是會多開一個後門,編譯器多一個查詢去處。

方法是確認函數呼叫的 argument 是哪個 namespace,然後檢查
arg-namespace::函數名(...) 是否存在。

所以明知道 istream& getline ( istream& is, string& str );
此函數全名是 std::getline 的情況下,直接用

#include <iostream>
#include <string>

int main() {
std::string buf;
getline(std::cin, buf);
return 0;
}

這樣寫也能順利呼叫 std::getline,因為 ADL 幫忙開了後門。

乍看之下,多了一個後門可以走好像很爽,而且只有 C++ 才能用,
其他兩大陣營的 JAVA / C# 都沒有。

但現在時代趨勢就是語言要換來換去,跟著大部隊的腳步走才
是正確的。好不容易 basic 語法沒落,現在大家的 for, while, if
語法終於能互通。平常因為 C++ 沒有垃圾收集,整天要記得寫 C++ 時
new 完要自己 delete 已經很累了,哪有心力為了 C++ 多記
一個函數名稱查詢規則?

無視 ADL,當這條規則不存在就沒事了嗎?
用程式碼驗證:

// #################################################

#include <stdio.h>
#include <iostream>

// #################################################

namespace air {

class CO2 { /* nothing inside */ };

} // namespace air

// #################################################

namespace air {

void foo(CO2 &obj) {
puts("in air::foo()");
}

} // namespace air

// #################################################

namespace myfavorite {

void foo(air::CO2 &obj) {
puts("in myfavorite::foo()");
}

} // namespace myfavorite

// #################################################

class CCLemon {
public:
void foo(air::CO2 &obj) {
puts("in CCLemon::foo()");
}

void FindMrRight(air::CO2 &obj) {
foo(obj);
}
};

// #################################################

int main() {

// To PROVE functions in the same class are higher than ADL.
// (While the function name is not fully qualified.)
air::CO2 obj;
CCLemon cc;
cc.FindMrRight(obj); // 最終會印出 in CCLemon::foo()

// To PROVE compile error (vc, gcc) occurs and
// the "using namespace" become useless because of ADL.
using namespace myfavorite;
foo(obj);

return 0;
}

// #################################################

其中關鍵是 return 0; 上方那兩行

using namespace myfavorite;
foo(obj);

既然 foo(obj); 沒有使用全名,且前方有 using,那按照 C++、JAVA、C#
一致同意的觀念,這裡應該要呼叫 myfavorite::foo(obj); 但因為
ADL 的查詢規則存在,所以 air::foo(obj); 也可行,因此會編譯錯誤。

可以使用全名解決,或者拿掉 air、myfavorite 其中一個的 foo。


如果 air 類別以及 air::foo 的作者是甲,其餘的部份都是乙寫的。
乙寫的部份幾百年前就寫好,本來都運行正常。
可是甲新增了 air::foo 後,就害乙這邊的不能編譯。

照理說先寫先贏,而且根據 namesapce 的觀念,本來乙寫的就正確用法,
沒有要乙改的道理,但是甲方那邊不需要依賴乙的 myfavorite,所以
不會碰到編譯錯誤,他只要裝傻說他那邊可以用,堅持他不必改,又該如何?

這就是 ADL 讓人詬病的地方,為了小小的方便,沒事開個後門,
導致無數衝突的發生。這跟 friend 很像,你讓 DLL 函數當 friend,
哪天資料有錯誤要抓病源時,就會因為該朋友函數不是你維護的而無力。


[h2] ADL 與 cout [/h2]

cout << 3.2F << cust::aMyCustomClassObj;

之所以能在不更改 ostream 類別原始碼的情況下,能夠如此流線地處理
你的自訂類別,是因為有全域函數可以用來自訂運算子,
所以這樣的用法,不會出現在 C#。

但是全域函數又可能定義在 namespace cust 裡,此時不破壞這一致性的
呼叫形式又要正確調用 cust::operator<<(...) 就只能是透過 ADL。

編譯器首先翻譯成

operator<<( operator<<(cout, 3.2F), cust::aMyCustomClassObj );

接著透過 ADL 後門得知,第一次要呼叫 std::operator<<
而第二次要呼叫 cust::operator<< 或 ::operator<<

如果沒有 ADL 的話,就沒有 cin, cout 的經典用法了,所以大概
新版的 C++ 標準不太可能拿掉這個東西。


--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 124.8.135.79

※ 發信站: 批踢踢實業坊(ptt.cc)
※ 轉錄者: purpose (124.8.135.79), 時間: 04/20/2012 02:57:38
LPH66:其實我們單寫 << 所呼叫的 operator << 是 non-qualified 的 04/20 03:08
LPH66:因此如果沒有 ADL, 在不用 using namespace std; 時, 04/20 03:09
LPH66:甚至連 cout << "Hello, World"; 都過不了 04/20 03:09
purpose:感謝L大,所以上面應該是 operator<<(std::cout, 3.2F) 04/20 03:22
purpose:補充:在 function template 內要呼叫全域函數時,ADL 也 04/20 08:13
purpose:必須用來解決名稱問題,因為參數型態未知,無法用全名呼叫 04/20 08:14

補充:

知名 C++ 大師 Herb Sutter 的文章 Namespaces & Interface Principle
http://www.gotw.ca/publications/mill08.htm

是經典的 ADL 論文。

其中要義1:自由函數 (free function) 即本文說的全域函數
要義2:因為 air::foo(air::CO2) 有用到 CO2 類別,所以雖然此全域
函數不是 CO2 類別的成員,在邏輯上還是能視為 CO2 介面的一部分,是為
所謂的 Interface Principle ( <s>腦補幻想症</s> 介面原則)。
※ 編輯: purpose 來自: 124.8.135.79 (04/20 09:57)
legnaleurc:ADL 對於 operator overloading 的影響很大, 也不能忽 04/20 16:07
legnaleurc:視它帶來的 package 效果, 只要搞清楚它的規則, 不必把 04/20 16:07
legnaleurc:它視為洪水猛獸 04/20 16:08
purpose:純 C 不需要 ADL,純 OO 也不需要 ADL,再加上他雖然提供 04/20 16:33
purpose:方便的方法解決不少問題,卻也帶來新的問題的情況下,我對 04/20 16:33
purpose:這 ADL 不太看好,我還是認為使用它是得不償失的 04/20 16:35
legnaleurc:以你舉的例子來看, 甲方使用 using air::foo; 也可解決 04/20 16:48
legnaleurc:啊, 抱歉, 我看錯邊了 04/20 16:49
legnaleurc:我認為你舉的例子有點矛盾, 因為 mf::foo 用到 air 04/20 16:51
legnaleurc:乙不可能在甲之前就先寫完啊? 04/20 16:51
legnaleurc:再者, 同一個 namespace 的整合性較強也是很合理的吧? 04/20 16:52
purpose:抱歉沒講清楚,我指的是 CO2 類別最先有,然後 mf:foo 出 04/20 17:07
purpose:現。最後才是 air::foo。同 namespace 整合性強,但是誰都 04/20 17:08
purpose:能去寫這個 air::foo 把自己函數加進 air 裡。更重要的是 04/20 17:09
purpose:我們原本對 using namespace 的觀念被破壞掉 04/20 17:09

你可能也想看看

搜尋相關網站