C++ Exceptional 名称查找、命名空间和接口原则

这一章的内容由下面一个例子引出:

namespace A
{
    struct X;
    struct Y;
    void f(int);
    void g(x);
}

namespace B
{
    void f(int i)
    {
        f(i); // which f
    }
    void g(A::X x)
    {
        g(x); // which g
    }
    void h(A::Y y)
    {
        h(y); //which h
    }
}

下面注意回答以上问题:
1. 很好回答,调用的是B中的f,也就是说调用的是自己。
2. 这里的调用时有歧义的,即这里需要明确指出调用的是A中的g还是B中的g。这个地方,大部分人可能会由疑问,这里难道不是该调用B中的g吗,为什么会和A中的g有歧义呢?为了解释这个问题,我们引出Koenig Lookup的概念:
如果你提供给一个函数的参数一个类型(在这里x具有类型A::X),那么编译器就会到相应的命名空间(这里是A)中去查找匹配的函数。

在这里,我们指定了x的类型A::X,所以编译器除了在默认的命名空间查找匹配的函数外,还会到A的命名空间中去查找相匹配的函数,所以这里就产生了歧义。

那么,问题是编译器为什么要这样做呢?
首先我们对类的定义如下:
一个类描述了一个数据的集合,和那些在这些数据上进行操作的函数。
首先回答下面一个问题:
一个类里有什么,或者说什么事类和接口的一部分?
很明显,类的成员变量、成员函数、静态函数等都是类和接口的一部分。
考虑下面一段代码:

//*** Example 1(a)
class X { /*...*/ }
/*...*/
void f( const X&);

//*** Example 1 (b)
class X
{
    /*...*/
public:
    void f() const;
}

认真思考一下上面两个f的区别,你会发现,除了一个是显示的用到X,一个是隐式的传一个指针外,这两个函数并没有任何的区别。所以,如果说第二个f算作是X的一部分的话,那么第一个f应该也算作X的一部分。

下面给出属于X的类的定义:
1. 用到了类X
2. 和类X在同一个文件或者同一个命名空间中。
只要满足以上这两个条件的函数,都可以看做是类X的一部分。

而编译器在看到满足以上两个条件的函数时,就会把此类和此函数之间建立一个关系。这里分两种情况:一种情况是此函数是类的成员函数,那么此类和此函数之间的关系就会强点儿;另一种情况是,此类不是类的成员函数,但此函数和类在同一个文件或者同一个命名空间中,那么此类和此函数之间的关系就会弱点。而编译器在选择用哪个函数时,肯定会选择关系强一点的那个函数。
而也正是因为类和函数之间的这种关系,C++才会利用Koenig Lookup 法则去匹配函数。

看下面这个例子:

//*** Example 
namespace NS
{
    class T {}
}
void f( NS::T); //global function
int main()
{
    NS::T param;
    f(param);
}

上面这个例子很明显,会调用全局的f函数。接着看下面这个例子:

namespace NS
{
    class T{ };
    void f(T);
}

void f(NS::T);

int main()
{
    NS::T param;
    f(param);
}

现在,很遗憾,这里的f就会出现歧义,因为f同时在NS命名空间和全局存在,所以产生了歧义。接着看下面一个例子:

namespace A
{
    class x{};
    void f(X);
}

namespace B
{
    void f(A::X);
    void g(A::X param)
    {
        f(param);
    }
}

同样,以上代码也会产生歧义,因为f同时在A和B命名空间中存在,所以会产生歧义。但下面的例子会是例外:

namespace A
{
    class X{};
    void f(X);
}

class B
{
    void f(A::X);
    void g(A::X param)
    {
        f(param);
    }
}

大家一眼看上去可能会说,这个代码和上面的有什么区别吗,不应该也有歧义吗?其实区别就在于这里的B是class而不是namespace,而f是作为一个成员函数存在的,而成员函数的优先级相比于命名空间中的是要高的,所以这里并没有歧义,而是会调用B::f(A::X)。

另外,要提到的是,我们在写类似于下面这种语句时

std::cout << i << std::endl;

我们并没有指明operator<<的命名空间(operator<<在std中),也没有写类似于using std::operator<<的语句,那怎么就通过编译了呢,其实这里用到的也正是Koenig Lookup 法则。

最后,我们再来说一下有关函数隐藏的知识,大家都知道如果在一个类的子类里声明了一个父类里的同名函数,但它们的参数是不一样的,那么子类里的函数就会隐藏掉父类里的函数,若是想调用父类里的同名函数,需要显示的把父类的函数引入到命名空间。为什么会这样呢?大家也许会问,既然它们的参数不同,那么在调用的时候应该能准确找到父类中的相应函数啊。其实这就涉及到了C++在寻找函数时所采用的一个规则,首先它会根据名称(这个时候不会考虑访问权限或者参数的问题)在自己的类里去找,找到后,它就不会继续寻找了。然后它再检查函数的访问权限、参数能否转换等问题。正是由于这个查找规则,才导致了以上的问题。

  • 综上所述,其实就三个关键点:
    要知道Koenig Lookup 法则,并知道此法则应用的两个条件:
    (1)函数用到了类
    (2)函数和类在同一个文件提供,或者在同一个命名空间中
    只有满足了以上条件,c++才会认为此函数和类是有一定关系的,
    在名称查找时,才能运用Koenig Lookup 法则。

  • 运用Koenig Lookup 法则关联的函数要比类的成员函数关系弱,c++会优先选择成员函数

  • 了解C++在寻找函数时具体的规则,首先是按照名称查找,这个时候是不考虑访问权限和参数是否匹配的问题的,找到后才会再去检查这些条件
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值