【Exceptional C++(18)】接口原则
2018-01-30 本文已影响10人
downdemo
- Class定义:一个Class描述了一组数据及操作这些数据的函数
class X { ... };
void f(const X&); // f是X的一部分吗?
// 如果上面的代码出现在一个头文件中
class X {
public:
void f() const; // f是X的一部分吗?
}
- 接口原则:对于一个类X,所有的函数,包括非成员函数,只要同时满足提到X且与X同期提供,那么就是X的逻辑组成部分,因为它们形成了X的接口。根据定义,所有成员函数都是X的组成部分
- 每个成员函数都必须提到X:非static的成员函数有一个隐含参数,类型是X* const或const X* const。static的成员函数也是在X的作用范围内的
- 每个成员函数都与X同期提供(在X的定义体内):f提到X,如果f与X同期存在(如存在于相同的头文件、命名空间),根据接口原则,f是X的逻辑组成部分,因为它是X接口的一部分
- 接口原则可以用于判断什么是一个class的组成部分
class X { ... };
ostream& operator<<(ostream&, const X&);
- 这样就很容易看出来,如果operator<<与X同期提供,那么operator<<是X的逻辑组成部分,因为它是X的接口的一部分,class的定义中并没有要求函数是否为成员
- 接口原则还有更深远的意义,如果你能认识到它和Koenig Lookup干了相同的事
namespace NS {
class T {};
void f(T);
}
NS::T parm;
int main() {
f(parm); // call NS::f
}
- 很明显f(parm)不需要明确写为NS::f(parm),但这对编译器来说并不明显,Koenig Lookup让编译器正确完成。Koenig Lookup规定如果传给函数一个class类型实参,为了查找这个函数名,编译器被要求不仅要搜索局部作用域,还要搜索包含实参类型的命名空间
- 不用明确限定f我们就很容易限定函数名
#include <iostream>
#include <string>
int main() {
std::string hello = "hello, world";
std::cout << hello; // call std::operator<<
}
- 如果没有Koenig Lookup编译器就无法找到operator<<,我们只知道它是string的一部分,如果必须强迫限定这个函数名会很麻烦,得写成std::operator<<(std::cout. hello)
- 回到接口原则,看这个Myers Example
namespace NS {
class T {};
}
void f(NS::T);
int main() {
NS::T parm;
f(parm); // call global f
}
- 有一天NS的作者需要增加一个函数
namespace NS {
class T {};
void f(T); // new function
}
void f(NS::T);
int main() {
NS::T parm;
f(parm); // ambiguous: call NS::f or global f?
}
- 在命名空间增加一个函数的行为破坏了命名空间外的代码,即使用户没有用using把NS带到作用域中。不过Nathan Myers(C++标准委员会的长期会员)指出了在命名空间与Koenig Lookup之间的关联
namespace A {
class X {};
}
namespace B {
void f(A::X);
void g(A::X parm) {
f(parm); // call B::f
}
}
// 增加一个函数
namespace A {
class X {};
void f(X); // new function
}
namespace B {
void f(A::X);
void g(A::X parm) {
f(parm); // ambiguous: call A::f or B::f
}
}
- 命名空间的作用就是为了防止名字冲突,但在一个命名空间中增加函数却看似破坏了另一个完全隔离的命名空间的代码。事实上,这是应该发生的正确行为。如果X所在的命名空间中有f(X),根据接口原则,f是X的接口的一部分,f是一个自由函数不是关键,想确认它仍然是X的逻辑组成部分只要换个名字看
namespace A {
class X {};
ostream& operator<<(ostream&, const X&);
}
namespace B {
ostream& operator<<(ostream&, const A::X&);
void g(A::X parm) {
cout << parm; // ambiguous: A::operator<< or B::operator<< ?
}
}
- 如果用户代码提供了一个提及X的函数,而它与X所处的命名空间的某个函数重合。B必须明确表明想调用的函数,这正是接口原则应该提供的东西,因此接口原则与Koenig Lookup的行为相同并非意外,Koenig Lookup的行为正是建立在接口原则基础上的
- 虽然接口原则说明成员和非成员函数都是class的逻辑组成部分,但并没有说明两者是平等的,在名称搜索中,成员函数与class之间的关联关系比非成员函数更强
namespace A {
class X { };
void f(X);
}
class B { // class, not namespace
void f(A::X);
void g(A::X parm) {
f(parm); // call B::f
}
};
- 如果A和B都是class,f(A, B)是一个自由函数
- 如果A与f同期提供,那么f是A的组成部分,且A将依赖B
- 如果B与f同期提供,那么f是B的组成部分,且B将依赖A
- 如果A、B、f同期提供,那么f同时是A和B的组成部分,且A与B是循环依赖
- 如果A和B都是class,且A::g(B)是A的一个成员函数
- 因为A::g(B)的存在,A总是依赖B
- 如果A和B是同期提供的,那么显然A::g(B)和B也是同期提供的,于是A::g(B)同时满足提及B和与B同期提供,所以根据接口原则A::g(B)是B的组成部分,又因为A::g(B)使用了一个隐含的A*参数,所以B依赖A,因此A和B循环依赖
- 与class不同,namespace不需要一次声明完毕,同期提供取决于namespace当前的可见部分
// file a.h
namespace N { class B; }
namespace N { class A; }
class N::A { public: void g(B); };
// file b.h
namespace N { class B { ... }; }
- A的用户包含了a.h,于是A和B是同期提供且循环依赖的,B的用户包含了b.h,于是A和B不是同期提供的