引子

先看一段C++初学者都会写的代码

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

int main(int argc, const char * argv[]) {
std::string str = "Hello, World!\n";
std::cout << str; // 1
std::cout << "Hello, World!\n"; // 2
return 0;
}

试着回答下面几个问题:

  • 如果之前学过其他语言但是没接触过C++,代码中<<的写法看起来很怪异,它是怎么工作的?
  • 1 & 2 的写法有什么不同?它们调用的是同一个方法么?

回答

C++中有一个神奇的东西叫做操作符重载,这个操作允许用户针对某一些需要的类型自定义操作符。比如说下面这个数据类型就重载了+号操作符,从而允许更加简单的代码书写形式。这里先不讨论操作符重载带来的问题,但是这个确实是在标准库中被普遍使用的,这里看到的<<也是属于操作符重载。

1
2
3
4
5
6
7
8
class Data
{
public:
Data& operator+(int i){std::cout << "Data+" << i; return *this;}
};

Data d;
d = d + 1;

我们可以在<ostream>中看到basic_ostream预定义了很多对于operator<<的操作符重载,可以简单的看一下,这里只摘录了一些,头文件中实际定义了更多类型。

1
2
3
4
5
6
7
8
9
// 27.7.2.6 Formatted output:
basic_ostream& operator<<(basic_ostream& (*pf)(basic_ostream&));
basic_ostream& operator<<(basic_ios<charT, traits>& (*pf)(basic_ios<charT,traits>&));
basic_ostream& operator<<(ios_base& (*pf)(ios_base&));
basic_ostream& operator<<(bool n);
basic_ostream& operator<<(short n);
basic_ostream& operator<<(unsigned short n);
basic_ostream& operator<<(int n);
...

那么1和2是否都调用了basic_ostream中预定的<<呢?2确实是调用了basic_ostream中的方法,2的代码可以改写成下面这种形式。

1
2
3
std::cout << "Hello, World!\n";
// 相当于
std::cout.operator<<("Hello, World!\n");

但1并不是调用basic_ostream中的方法。basic_ostream中预定义的都是系统自带的类型,而std::string则是标准库自定义的类型,basic_ostream并不知道它的存在。那么std::cout << str;这行代码是怎么工作的呢?

我这里先给出答案,然后在慢慢的解释是如何工作的

1
2
3
4
5
std::string str = "Hello, World!\n";
std::cout << str;
// 相当于
operator<<(std::cout, str);
//

Argument-Dependent Lookup(实参相关的查找)

中文翻译参考自C++ Primer,下面简称ADL。这个查找规则简单的可以归纳为:

当我们给函数传递一个类类型的对象时,出了在常规的作用域查找外还会查找实参类所属的命名空间。这个规则对于传递类的引用或指针的调用同样有效。 - 《C++ Primer 5th》

举个例子就能够很直观的看到这个效果。

1
2
3
4
5
6
7
8
9
10
namespace NS 
{
class A {};
void f( A &a, int i) {}
}
int main()
{
NS::A a;
f( a, 0 ); //calls NS::f
}

有了这个规则以后我们就很好理解本文开头的第一个例子如何工作的。<string>头文件中定一个一个operator<<的操作符重载并且接受basic_ostream为第一个参数,std::string为第二个参数。当编译器在basic_ostream中找不到对应的函数的时候,它便开始在std::string的命名空间下查找对应的方法。然后它找到中有一个符合要求的方法,然后就调用了这个方法。关于C++的命名查找规则是一个巨复杂的东西,有兴趣可以看看这个页面(Name Lookup

同理,我们可以为我们自己的类型自定义这个操作符重载,这样我们在输出的时候就可以更加的方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person
{
public:
std::string name;
int age;
};

std::ostream& operator<<(std::ostream& os,
const Person &p)
{
os << "name: " << p.name << " age: " << p.age;
return os;
}

Person p{"john", 100};
std::cout << p << std::endl;

关于ADL我们需要注意什么

能够被ADL找到的非成员方法应该被视为类的设计的一部分,如果你考虑写类似的方法如果不是为了ADL的话应该避免这种写法。

ADL能够极大的方便我们编码时候的方法调用,从而提高代码的可读性。STL库内使用的swap方法也广泛的使用了这个技术。关于swap的话题,改天单独讨论。

参考资料