公共朋友交换成员函数

copy-and-swap-idiom的漂亮答案中,有一段代码我需要一些帮助:

class dumb_array
{
public:
// ...
friend void swap(dumb_array& first, dumb_array& second) // nothrow
{
using std::swap;
swap(first.mSize, second.mSize);
swap(first.mArray, second.mArray);
}
// ...
};

他又加了一句

还有其他主张,我们应该专门为我们的类型std::swap,提供类内交换以及自由函数交换等等。但这都是不必要的:任何正确的swap使用都将通过一个不合格的调用,并且我们的函数将通过ADL找到。一个函数就可以了。

我必须承认,对于friend,我有点“不友好”。所以,我的主要问题是:

  • 看起来像一个自由函数,但它在类体?
  • 为什么这个swap不是静态的吗?它显然没有使用任何成员变量。
  • “任何正确使用掉期都会通过ADL找到掉期”吗?ADL会搜索名称空间,对吧?但是它也可以查看类内部吗?或者这里是friend出现的地方?

Side-questions:

  • 在c++ 11中,我应该用noexcept标记我的__abc0吗?
  • 对于c++ 11及其范围,我应该在类中以同样的方式放置friend iter begin()friend iter end()吗?我认为friend在这里不需要,对吗?
38215 次浏览

该代码等价于(在几乎中):

class dumb_array
{
public:
// ...
friend void swap(dumb_array& first, dumb_array& second);
// ...
};


inline void swap(dumb_array& first, dumb_array& second) // nothrow
{
using std::swap;
swap(first.mSize, second.mSize);
swap(first.mArray, second.mArray);
}

在类中定义的友元函数是:

  • 放置在封闭的名称空间中
  • 自动inline
  • 无需进一步限定就可以引用类的静态成员

确切的规则在[class.friend]节(我引用c++ 0x草案的第6和7段):

函数可以在类的友元声明中定义,当且仅当类是非局部类(9.8),函数名是限定的,并且函数具有命名空间作用域。

这样的函数是隐式内联的。类中定义的友元函数在定义它的类的(词法)作用域中。在类外部定义的友元函数则不是。

有几种方法来编写swap,有些方法比其他方法更好。不过,随着时间的推移,人们发现单一的定义效果最好。让我们考虑如何编写swap函数。


我们首先看到像std::vector<>这样的容器有一个单参数成员函数swap,例如:

struct vector
{
void swap(vector&) { /* swap members */ }
};

当然,我们的班级也应该如此,对吧?其实不是。标准库有各种不必要的事情,成员swap是其中之一。为什么?让我们继续。


我们应该做的是确定什么是规范的,以及我们的类需要如何处理它。而交换的规范方法是使用std::swap。这就是为什么成员函数没有用处:一般来说,它们不是我们交换东西的方式,并且与std::swap的行为没有关系。

那么,为了使std::swap工作,我们应该提供(并且std::vector<>应该已经提供)std::swap的专门化,对吗?

namespace std
{
template <> // important! specialization in std is OK, overloading is UB
void swap(myclass&, myclass&)
{
// swap
}
}

在这种情况下,这当然是可行的,但它有一个明显的问题:函数特殊化不能是局部的。也就是说,我们不能用这个来专门化模板类,只能用特定的实例化:

namespace std
{
template <typename T>
void swap<T>(myclass<T>&, myclass<T>&) // error! no partial specialization
{
// swap
}
}

这种方法有时有效,但并非总是有效。一定有更好的办法。


有!我们可以使用friend函数,并通过诽谤联盟找到它:

namespace xyz
{
struct myclass
{
friend void swap(myclass&, myclass&);
};
}

当我们想要交换一些东西时,我们关联__abc1 std::swap,然后进行一个非限定调用:

using std::swap; // allow use of std::swap...
swap(x, y); // ...but select overloads, first


// that is, if swap(x, y) finds a better match, via ADL, it
// will use that instead; otherwise it falls back to std::swap

friend函数是什么?在这个领域存在着困惑。

在c++标准化之前,friend函数做了一些叫做“朋友名注入”的事情,如果函数写在周围的命名空间中,代码就会表现为好像。例如,这些都是等效的前置标准:

struct foo
{
friend void bar()
{
// baz
}
};


// turned into, pre-standard:


struct foo
{
friend void bar();
};


void bar()
{
// baz
}

然而,当诽谤联盟被发明时,它被删除了。friend函数可以通过ADL找到只有;如果你想让它作为一个自由函数,它需要这样声明(例如看到这个)。但瞧!有个问题。

如果你只使用std::swap(x, y),你的重载将会被找到从来没有,因为你已经显式地说了“在std中查找,而没有其他地方”!这就是为什么有些人建议写两个函数:一个是通过诽谤联盟找到的函数,另一个是处理显式std::限定的函数。

但正如我们所看到的,这并不是在所有情况下都有效,我们最终会得到一个丑陋的烂摊子。相反,习惯交换走了另一条路:不是让类的工作来提供std::swap,而是让交换者的工作来确保他们不使用合格的swap,就像上面那样。只要人们知道它,这种方法就会很有效。但问题就在这里:需要使用一个不合格的调用是不直观的!

为了使这更容易,一些库(如Boost)提供了boost::swap函数,该函数只对swap进行非限定调用,并将std::swap作为关联的命名空间。这有助于使事情再次简洁,但它仍然是一个遗憾。

注意,c++ 11中std::swap的行为没有变化,而我和其他人错误地认为情况会是这样。如果你被这个咬了,在这里阅读


简而言之:成员函数只是噪音,专门化是丑陋的和不完整的,但friend函数是完整的和工作的。交换时,要么使用boost::swap,要么使用附带std::swap的非限定swap


†非正式地说,如果一个名称将在函数调用期间被考虑,则该名称为相关的。详情请参见§3.4.2。在这种情况下,std::swap通常不被考虑;但是我们可以联系它(将它添加到不合格的swap考虑的重载集),允许它被找到。