Programming 板


LINE

之前这篇文章 #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 purpose:转录至看板 C_and_CPP 04/20 02:57
1F:推 christianSK:长知识了~ 推 140.114.71.192 04/20 09:27
2F:推 lovesnake:推~ 原来平常不以为意的东西这麽复杂 140.121.216.68 04/20 12:39







like.gif 您可能会有兴趣的文章
icon.png[问题/行为] 猫晚上进房间会不会有憋尿问题
icon.pngRe: [闲聊] 选了错误的女孩成为魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一张
icon.png[心得] EMS高领长版毛衣.墨小楼MC1002
icon.png[分享] 丹龙隔热纸GE55+33+22
icon.png[问题] 清洗洗衣机
icon.png[寻物] 窗台下的空间
icon.png[闲聊] 双极の女神1 木魔爵
icon.png[售车] 新竹 1997 march 1297cc 白色 四门
icon.png[讨论] 能从照片感受到摄影者心情吗
icon.png[狂贺] 贺贺贺贺 贺!岛村卯月!总选举NO.1
icon.png[难过] 羡慕白皮肤的女生
icon.png阅读文章
icon.png[黑特]
icon.png[问题] SBK S1安装於安全帽位置
icon.png[分享] 旧woo100绝版开箱!!
icon.pngRe: [无言] 关於小包卫生纸
icon.png[开箱] E5-2683V3 RX480Strix 快睿C1 简单测试
icon.png[心得] 苍の海贼龙 地狱 执行者16PT
icon.png[售车] 1999年Virage iO 1.8EXi
icon.png[心得] 挑战33 LV10 狮子座pt solo
icon.png[闲聊] 手把手教你不被桶之新手主购教学
icon.png[分享] Civic Type R 量产版官方照无预警流出
icon.png[售车] Golf 4 2.0 银色 自排
icon.png[出售] Graco提篮汽座(有底座)2000元诚可议
icon.png[问题] 请问补牙材质掉了还能再补吗?(台中半年内
icon.png[问题] 44th 单曲 生写竟然都给重复的啊啊!
icon.png[心得] 华南红卡/icash 核卡
icon.png[问题] 拔牙矫正这样正常吗
icon.png[赠送] 老莫高业 初业 102年版
icon.png[情报] 三大行动支付 本季掀战火
icon.png[宝宝] 博客来Amos水蜡笔5/1特价五折
icon.pngRe: [心得] 新鲜人一些面试分享
icon.png[心得] 苍の海贼龙 地狱 麒麟25PT
icon.pngRe: [闲聊] (君の名は。雷慎入) 君名二创漫画翻译
icon.pngRe: [闲聊] OGN中场影片:失踪人口局 (英文字幕)
icon.png[问题] 台湾大哥大4G讯号差
icon.png[出售] [全国]全新千寻侘草LED灯, 水草

请输入看板名称,例如:Gossiping站内搜寻

TOP