有几种编写swap的方法,有些比其他的更好。不过,随着时间的推移,人们发现单一的定义效果最好。让我们考虑一下如何编写swap函数。
我们首先看到std::vector<>这样的容器有一个单参数成员函数swap,例如:
struct vector
{
void swap(vector&) { /* swap members */ }
};
当然,我们的班级也应该如此,对吧?其实不是。标准库中有各种不必要的东西,成员交换就是其中之一。为什么?让我们继续。
我们应该做的是确定什么是规范的,以及我们的类需要做什么来处理它。交换的规范方法是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
}
}
这种方法有时有效,但并非总是有效。一定有更好的办法。
有!我们可以使用好友函数,通过ADL找到它:
namespace xyz
{
struct myclass
{
friend void swap(myclass&, myclass&);
};
}
当我们想要交换一些东西时,我们关联†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
什么是好友功能?在这个领域存在着困惑。
在c++标准化之前,友元函数做了一些叫做“友元名称注入”的事情,代码表现得就好像函数是在周围的命名空间中编写的一样。例如,这些都是等效的前置标准:
struct foo
{
friend void bar()
{
// baz
}
};
// turned into, pre-standard:
struct foo
{
friend void bar();
};
void bar()
{
// baz
}
然而,当ADL被发明时,这被删除了。好友功能只能通过ADL找到;如果您希望它是一个自由函数,则需要将其声明为自由函数(例如,请参阅下面的示例)。但瞧!有个问题。
如果你只是使用std::swap(x, y),你的重载将永远不会被发现,因为你已经明确地说了“在std中查找,而没有其他地方”!这就是为什么有些人建议写两个函数:一个是通过ADL找到的函数,另一个是处理明确的std:: qualitions。
但正如我们所看到的,这并不是在所有情况下都有效,我们最终会得到一个丑陋的烂摊子。相反,习惯交换走了另一条路:不是让类的工作是提供std::swap,而是让交换者的工作是确保他们不使用合格的交换,就像上面那样。只要人们知道它,这种方法就会很有效。但问题就在这里:需要使用一个不合格的调用是不直观的!
为了简化这一点,一些库(如Boost)提供了Boost::swap函数,该函数只对swap进行非限定调用,并将std::swap作为关联的名称空间。这有助于使事情再次简洁,但它仍然是一个遗憾。
注意,c++ 11中std::swap的行为并没有改变,我和其他人错误地认为情况会是这样。如果你被这个咬了,请阅读这里。
简而言之:成员函数只是噪音,专门化是丑陋和不完整的,但朋友函数是完整的和工作的。交换时,要么使用boost::swap,要么使用与std::swap相关的非限定交换。
†非正式地说,如果一个名称将在函数调用期间被考虑,则该名称是关联的。详情请参见§3.4.2。在这种情况下,std::swap通常不被考虑;但是我们可以关联它(将它添加到非限定交换所考虑的重载集),从而允许找到它。