如何重载std :: swap()函数?

128

std::swap()被许多标准库容器(例如std::liststd::vector)在排序和赋值过程中使用。

然而,该函数的标准实现非常通用,对于自定义类型而言效率较低。

因此,可以通过为自定义类型重载std::swap()函数来获得更高的效率。但是如何实现以便被标准库容器所使用呢?


Swappable页面已移至https://en.cppreference.com/w/cpp/named_req/Swappable。 - Andrew Keeton
4个回答

152

重载 std::swap 的正确方式(也称为特化它)是在与需要交换的内容相同的命名空间中编写它,以便可以通过参数相关查找(ADL)找到它。一个特别容易做的事情是:

class X
{
    // ...
    friend void swap(X& a, X& b)
    {
        using std::swap; // bring in swap for built-in types

        swap(a.base1, b.base1);
        swap(a.base2, b.base2);
        // ...
        swap(a.member1, b.member1);
        swap(a.member2, b.member2);
        // ...
    }
};

12
在C++2003标准中,swap函数的规定非常不明确。大多数实现确实使用ADL来查找swap,但它并没有被强制要求,因此您不能指望它会总是起作用。您可以像OP所示那样为特定具体类型专门定义std::swap函数;但是,不要期望该专门化能够用于该类型的派生类。 - Dave Abrahams
18
如果实现仍然没有使用ADL来找到正确的swap,我会感到惊讶。这是委员会上的一个老问题。如果您的实现没有使用ADL来找到swap,请提交一个错误报告。 - Howard Hinnant
5
取决于情况。使用ADL交换元素的std::sort在C++03中不符合规范,但在C++11中符合规范。此外,为什么要基于客户端可能使用非惯用代码来否定一个答案? - JoeG
5
如果仅仅阅读规范就能理解,那么@curiousguy说的没错 :-). 但很遗憾,作者的意图也非常重要。如果原始意图是允许或应该使用ADL,则规范说明不足。如果不是,则这只是C++0x中一个普通的破坏性变化,这就是为什么我写“最好”的原因。 - Dave Abrahams
5
是的,他有消息来源!他是Dave Abrahams - Paolo M
显示剩余12条评论

80

注意Mozza314

这是一个模拟泛型std::algorithm调用std::swap并让用户在namespace std中提供其交换的效果。由于这是一个实验,因此此模拟使用namespace exp而不是namespace std

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            exp::swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

namespace exp
{
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

对于我来说,这会打印出:
generic exp::swap

如果你的编译器输出不同,那么它就没有正确实现模板的“两阶段查找”。如果你的编译器符合C++98/03/11标准中的任何一个,那么它将给出与我展示的相同的输出。在这种情况下,你担心的事情确实会发生。而将你的swap放入命名空间stdexp)也无法阻止它发生。
Dave和我都是委员会成员,已经在标准的这个领域工作了十年(并不总是达成一致意见)。但是这个问题已经解决很久了,我们都同意如何解决。如果你忽视Dave在这个领域的专业意见/答案,那么后果自负。
这个问题在C++98发布后引起了注意。从2001年开始,Dave和我开始研究这个领域。这是现代的解决方案:
// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

void swap(A&, A&)
{
    printf("swap(A, A)\n");
}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

输出结果为:

swap(A, A)

更新

发现以下观察结果:

namespace exp
{    
    template <>
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

它有效!那为什么不使用呢?

考虑一下当你的A是一个类模板时的情况:

// simulate user code which includes <algorithm>

template <class T>
struct A
{
};

namespace exp
{

    template <class T>
    void swap(A<T>&, A<T>&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A<int> a[2];
    exp::algorithm(a, a+2);
}

现在它又不起作用了。:-(
因此,您可以将swap放入命名空间std中,并使其正常工作。但是,对于存在模板的情况A<T>,您需要记住将swap放入A的命名空间中。由于如果将swap放入A的命名空间中,两种情况都可以工作,因此仅以这种方式执行更容易记住(并教给其他人)。

4
非常感谢您提供的详细答案。我对此了解不足,实际上想知道重载和特化如何产生不同的行为。然而,我并不建议重载,而是特化。当我在您的第一个示例中加入 template <> 后,gcc 的输出是 exp::swap(A, A)。那么,为什么不选择特化呢? - voltrevo
3
在课堂朋友语法方面应该没有问题。我建议在头文件中的函数范围内尽量少使用 using std::swap。是的,swap 几乎是一个关键字,但并不完全是。因此,在确实需要时才将其导出到所有命名空间中最好。swap 就像 operator== 一样。最大的区别是几乎没有人会考虑使用限定命名空间语法调用 operator==(它只是太丑陋了)。 - Howard Hinnant
16
你所看到的复杂性实际上只是太多错误答案的结果。Dave Abrahams 的正确答案并不复杂:"重载 swap 的正确方法是在与你要交换的东西相同的命名空间中编写它,这样可以通过参数相关查找(ADL)找到它。" - Howard Hinnant
2
Visual Studio尚未正确实现C++98引入的两阶段查找规则。这意味着在此示例中,VS调用了错误的swap函数。这增加了一个我以前没有考虑过的新问题:对于template<class T> struct A情况,将您的swap函数放入命名空间std可能会使您的代码不可移植。请在wandbox上尝试您的示例,以查看gcc和clang如何处理它。 - Howard Hinnant
显示剩余14条评论

55

根据C++标准,您不被允许重载std::swap,但是您可以专门为自己的类型添加模板特化到std名称空间中。例如:

namespace std
{
    template<>
    void swap(my_type& lhs, my_type& rhs)
    {
       // ... blah
    }
}

那么在std容器(以及任何其他地方)中的用法将选择您的专业化而不是一般化。

另请注意,为交换提供基类实现对于您的派生类型来说并不足够。例如,如果您有

class Base
{
    // ... stuff ...
}
class Derived : public Base
{
    // ... stuff ...
}

namespace std
{
    template<>
    void swap(Base& lha, Base& rhs)
    {
       // ...
    }
}

这对于基类会有效,但是如果您尝试交换两个派生对象,它将使用std中的通用版本,因为模板化的交换是一个精确匹配(并且避免了仅交换派生对象的“基础”部分的问题)。

注意:我已更新此内容,以删除上一个答案中的错误部分。谢谢puetzk和j_random_hacker指出。


14
因为正确的自定义交换空间的方式是在您自己的命名空间中进行(正如Dave Abrahams在另一个答案中指出的),所以被下投票了。 - Howard Hinnant
std::swap命名空间之外,重载std::swap(或其他任何东西)是否被禁止? - Kos
15
@HowardHinnant和Dave Abrahams:我不同意你们的观点。你们认为你们提出的替代方案是“正确”的依据是什么?正如puetzk从标准中引用的那样,这种方式是明确允许的。虽然我对这个问题还不熟悉,但我不喜欢你们提倡的方法,因为如果我使用那种方式定义了Foo并进行交换,那么使用我的代码的其他人可能会在Foo上使用std::swap(a, b)而不是swap(a, b),这将悄无声息地使用低效的默认版本。 - voltrevo
5
评论区的空间和格式限制不允许我完整回复您。请查看我添加的标题为“Attention Mozza314”的答案。 - Howard Hinnant
1
@HowardHinnant,我想这种技术也很容易违反一次定义规则,对吗?如果一个翻译单元已经包含了<algorithm>和类Base的前向声明,而另一个翻译单元包含了上面的头文件,则会有两个不同的std::swap<Base>实例。我记得在符合规范的程序中是禁止这样做的,但使用这种技术意味着您必须成功地防止您的类的用户编写前向声明 - 他们必须以某种方式被强制始终包括您的头文件以实现其目标。结果证明,在规模上实现这一点是不切实际的。 - codeshot
我相信从C++20开始,只有对模板类进行特化才是正确的,而不是模板函数。 - Chris_F

31
虽然一般不应该向std::命名空间中添加内容,但是允许为用户定义的类型添加模板特化。而重载函数则不被允许。这是一个微妙的区别 :-)
17.4.3.1/1规定:除非另有规定,否则C++程序不能向std或namespace std命名空间添加声明或定义。程序可以为任何标准库模板添加模板特化到std命名空间中。这样的标准库模板特化(完全或部分)会导致未定义的行为,除非声明依赖于具有外部链接的用户定义名称并且模板特化符合原始模板的标准库要求。
std::swap的一个特化看起来像:
namespace std
{
    template<>
    void swap(myspace::mytype& a, myspace::mytype& b) { ... }
}

没有模板<>的话,它将是一种未定义的过载,而不是允许的特化。@Wilka建议更改默认命名空间的方法可能适用于用户代码(由于Koenig查找更喜欢无命名空间版本),但这并不保证,并且实际上并不应该(STL实现应该使用完全限定的std::swap)。

有一个comp.lang.c++.moderated上的主题线程,其中有一个关于该主题的讨论。大部分讨论都是关于部分特化的,虽然目前还没有很好的方法可以做到。


7
使用函数模板特化来解决这个问题(或任何问题)是错误的一个原因是:它与重载的交互方式很糟糕,而针对swap有许多重载。例如,如果你为std::vector<mytype>&特化了常规的std::swap,由于特化在重载决议期间不被考虑,你的特化不会被选择覆盖标准的向量特定swap。 - Dave Abrahams
4
这也是梅耶斯在《Effective C++第三版》中推荐的做法(第25条,106-112页)。 - jww
2
@DaveAbrahams:如果你进行特化(没有显式的模板参数),那么部分排序将导致它成为“vector”版本的特化,并且[将被使用](https://wandbox.org/permlink/dmHLwU5uxmFKtwDw)。 - Davis Herring
1
@DavisHerring 实际上,当你这样做时,部分排序不起作用。问题不在于你根本无法调用它;而是在存在明显不太具体的交换重载时会发生什么:https://wandbox.org/permlink/nck8BkG0WPlRtavV - Dave Abrahams
2
@DaveAbrahams:部分排序是为了在显式特化匹配多个模板时选择要专门化的函数模板。您添加的::swap重载比vectorstd::swap重载更为专业,因此它捕获了调用,后者的任何专业化都不相关。我不确定这是一个实际问题(但我也不声称这是一个好主意!)。 - Davis Herring
@DavisHerring,啊,好的,是的;我不记得std::vector有一个swap重载。这是一个实际问题,因为程序员认为他正在为特定类型定制swap的行为,但他的定制可能会被不够具体的重载所替代。 - Dave Abrahams

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接