Hidden friends in C++

use friend while overloading operators for struct

MSVC 对像 std::setw 这样的流操纵符的实现需要重载 operator>><<。这种重载使用了一种称为“隐藏友元惯用法 (Hidden Friend Idiom)”的编码方法来加快编译时间。实际上,隐藏友元被广泛使用以提高编译效率。

隐藏友元是一个在类定义中作为友元定义的自由函数(通常是运算符重载)。有的时候这是必要的。例如,

class T1
{
private:
	int member;
public:
	std::ostream & operator<<(std::ostream & os)
	{
		os << member;
		return os;
	}
};

operator<< 被重载为 std::ostream。这意味着如果我们想打印出 member,我们应该像这样写:

T1 t;
t << std::cout;

这比较奇怪。但是如果我们使用一个友元,

public:
	friend std::ostream & operator<<(std::ostream & os, T1 tt)
    {
        os << tt.member;
        return os;
    }

这样就可以写比较正常的 std::cout << t; 了。

然而,如果对象是一个结构体,那么就不需要友元,因为所有成员都是 public 的。

struct T2 {int member;};

std::ostream & operator<<(std::ostream & os, T2 tt)
{
	os << tt.member;
	return os;
}

学 OI 的时候似乎大家都这样重载,从来没有考虑过其他的写法。

但是一个常常被忽略的关键点是,重载解析需要编译器花费大量时间来查找函数。如果我们在块作用域中声明 operator<< 作为友元,编译器需要选择的重载集就会更小。例子:

struct Foo
{
    int member;
    friend std::ostream &operator<<(std::ostream &os, const Foo &f) // fn1
    {
        os << f.member;
        return os;
    }
};

std::ostream &operator<<(std::ostream &os, int value) { /* ... */ } //fn2

int main()
{
    int i = 42;
    Foo f = createFoo();

    std::cout << i;  // #1
    std::cout << f;  // #2

    return 0;
}

在 #1 处,编译器只能看到 fn1。在 #2 处,编译器将使用参数相关查找(Argument-Dependent Lookup, ADL)来找到 fn2。正常的重载解析开销非常大。使用隐藏友元,我们可以减少这个开销。