命名空间 namespace
参数依赖查找 argument-dependent lookup (ADL)
命名空间很有用,但要小心使用。
【命名空间使用规则】如果你把一个类放在一个命名空间中, 那么最好也把和这个类相关的函数和操作符重载也放在这个命名空间中,否则可能有意想不到的错误发生。
引例: 下面这段代码有什么问题, 能编译过吗?
// Example 1: Will this compile?
//
// in some library header
namespace N { class C {}; }
int operator+(int i, N::C) { return i+1; }
// a mainline to exercise it
#include <numeric>
int main() {
N::C a[10];
std::accumulate(a, a+10, 0);
}
先看一个类继承当中隐藏的例子
// Example 2a: Hiding a name
// from a base class
//
struct B {
int f( int );
int f( double );
int g( int );
};
struct D : public B {
private:
int g( std::string, bool );
};
D d;
int i;
d.f(i); // ok, means B::f(int)
d.g(i); // error: g takes 2 args
这个例子中发生了名字隐藏,当我们在继承类D中声明函数g的时候, 就会自动隐藏其基类及间接基类中的同名函数g。 最后一行的d.g(i)编译错误”takes 2 args", 即使D::g是private和参数不匹配的。
编译器在遇到d.g(i)做了以下事情: 1. 首先检查当前作用域(本例是类D的作用域), 列出左右名字为g的函数列表,不管是否可访问,参数是否匹配; 2. 如果第一步一个函数都没找到, 则向外层作用域(本例是类B的作用域),以同样的方式继续寻找, 直到找到至少一个候选函数,或者作用域用完(全局作用域中也没找到)停止。 3. 编译器在找到的候选函数中执行重载匹配和访问控制。
有两种方法可以解决Example 2中的问题。
一种是显示的指定调用那个命名空间的函数; 一种是using 声明。 就本例这种情况来说,推荐第二种。
// Example 2b: Asking for a name
// from a base class
//
D d;
int i;
d.f(i); // ok, means B::f(int)
d.B::g(i); // ok, asks for B::g(int)
// Example 2c: Un-hiding a name
// from a base class
//
struct D : public B {
using B::g;
private:
int g( std::string, bool );
};
回到文章开头Example1的问题
问题是:Example1的代码能编译过吗? 答案是:有时候可以, 有时候不行。取决于具体上线文的实现。
如果只有Exmaple1的这么多代码,本人试过,是可以编译过的。 但是我加一个最常用的#include <iostream>, 就编不过了。 这是为什么?
问题的关键是要理解编译器在std::accumulate中做了什么:
namespace std {
template<class Iter, class T>
inline T accumulate( Iter first,
Iter last,
T value ) {
while( first != last ) {
value = value + *first; // 1
++first;
}
return value;
}
}
Example1中的代码实际调用的是std::accumulate<N::C*,int>, 然后代码 “value + *first” 需要调用接收int类型和N::C类型(或者可以转换为int和N::C类型)的operator+函数。 刚好我们在全局作用域有一个这样的函数operator+(int,N::C), 皆大欢喜?
很遗憾,不行! 问题是编译器可能可以,也可能不可以, 看到这个全局作用域的函数 operator+(int,N::C), 这取决于实例化std::accumulate<N::C*,int>时,当前作用域std中是否有同名operator+函数。
简单来说,根据前面讲得查找原则, 只要std中有同名函数operator+,不管参数,不管访问控制, 只要有,编译器就停止向上层作用域查找, 因此看不到globle作用域当中的operator+(int,N::C)。
这就是为什么我#include <iostream>就编译错误的愿意, iostrem中std作用域中一定有一个operator+函数。
解决办法
【参数依赖查找】命名空间查找有一个例外, 如果函数参数是一个class类型,则需要被查找的namespace和class还包含:
1), 该class本身
2), 该class的基类
3), 如果该class是另外一个class的member class,那么也包含该class上层class.
4), 以及该类所在的namespace
因此,我们可以把operator+和类C放在同一个命名空间中。 这符合文章开头所说的命名空间使用规则。
// Example 1b: Solution
//
// in some library header
namespace N {
class C {};
int operator+(int i, N::C) { return i+1; }
}
// a mainline to exercise it
#include <numeric>
#include <iostrem> // Ok even we include another header file
int main() {
N::C a[10];
std::accumulate(a, a+10, 0); // now ok
}
有趣的编译错误
在Example1未修改之前, 我们编译器报错为:
error C2784: 'class std::reverse_iterator<`template-parameter-1', `template-parameter-2', `template-parameter-3', `template-parameter-4', `template-parameter-5'> __cdecl std::operator +(template-parameter-5, const class std::reverse_iterator< `template-parameter-1', `template-parameter-2', `template-parameter-3', `template-parameter-4', `template-parameter-5'>&)' : could not deduce template argument for 'template-parameter-5' from 'int'
error C2677: binary '+' : no global operator defined which takes type 'class N::C' (or there is no acceptable conversion)
这虽然是由于没找到operator+的原因导致的 但是编译错误却很奇怪。
类似的, 如果还有一个常见的错误例子是:
#include <iostream>
#include <sstream>
#include <iterator>
#include <vector>
#include <algorithm>
using namespace std ;
namespace skg {
struct Triplet {
int x, y, z ;
Triplet (const int& p_x, const int& p_y, const int& p_z)
: x(p_x), y(p_y), z(p_z) { }
} ;
}
using namespace skg;
ostream& operator<< (ostream& os, const skg::Triplet& p_t) {
os << '(' << p_t.x << ',' << p_t.y << ',' << p_t.z << ')' ;
return os ;
}
void printVector() {
vector< Triplet > vti ;
vti.push_back (Triplet (1, 2, 3)) ;
vti.push_back (Triplet (5, 5, 66)) ;
copy (vti.begin(), vti.end(), ostream_iterator<Triplet> (cout, "\n")) ; // error compile
std::cout << vti[0] << vti[1] << std::endl; // ok
}
int main (void) {
printVector() ;
Triplet t(1, 2, 3);
std::cout << t << std::endl;// ok
}
没有将ostream& operator<< (ostream& os, const skg::Triplet& p_t)放到namespace skg中
编译错误为:
from test68-namespace-overload-cout.cpp:3:
/usr/include/c++/4.8/bits/stream_iterator.h: In instantiation of ‘std::ostream_iterator<_Tp, _CharT, _Traits>& std::ostream_iterator<_Tp, _CharT, _Traits>::operator=(const _Tp&) [with _Tp = skg::Triplet; _CharT = char; _Traits = std::char_traits<char>]’:...
In file included from /usr/include/c++/4.8/iostream:39:0,
from test68-namespace-overload-cout.cpp:1:
/usr/include/c++/4.8/ostream:602:5: error: initializing argument 1 of ‘std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&&, const _Tp&) [with _CharT = char; _Traits = std::char_traits<char>; _Tp = skg::Triplet]’
operator<<(basic_ostream<_CharT, _Traits>&& __os, const _Tp& __x)
编译错误信息令人费解,原因是匹配到一个错误的operator<<上,但是参数有不能匹配,报错参数错误,如: cannot bind ‘std::ostream_iterator<skg::Triplet>::ostream_type {aka std::basic_ostream<char>}’ lvalue to ‘std::basic_ostream<char>&&’, 但实际是namespace的问题, 新手容易在这里掉坑里。
Ref