作者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
※ 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