如何重载std :: swap()


115

std::swap()在排序甚至分配期间,许多std容器(例如std::liststd::vector)都使用它。

但是std的实现swap()非常笼统,对于自定义类型而言效率很低。

因此,可以通过std::swap()使用自定义类型特定的实现进行重载来获得效率。但是如何实现它,以便将其用于std容器?



Answers:


135

重载交换的正确方法是将其写入与交换内容相同的名称空间中,以便可以通过依赖于参数的查找(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);
        // ...
    }
};

11
在C ++ 2003中,它充其量是未指定的。大多数实现都使用ADL来查找交换,但是没有强制要求,所以您不能指望它。您可以将 std :: swap专用于特定的具体类型,如OP所示;只是不要指望这种专业化会被使用,例如用于该类型的派生类。
戴夫·亚伯拉罕斯

15
我会惊讶地发现实现仍未使用ADL来找到正确的交换。这是委员会的一个问题。如果您的实现未使用ADL查找交换,请提交错误报告。
Howard Hinnant

3
@Sascha:首先,我在命名空间范围内定义函数,因为那是唯一与通用代码有关的定义。因为int等。等 没有/没有成员函数,std :: sort等。等 必须使用免费功能;他们建立协议。其次,我不知道您为什么反对使用两个实现,但是如果您不接受非成员交换,那么大多数类注定会被低效率地排序。重载规则确保如果同时看到这两个声明,则在不加限定的情况下调用swap时,将选择更具体的一个(此声明)。
戴夫·亚伯拉罕斯

5
@ Mozza314:这取决于。std::sort使用ADL交换元素的A 不符合C ++ 03,但符合C ++ 11。另外,为什么要基于客户端可能使用非惯用代码的事实来为答案-1呢?
JoeG 2011年

4
@curiousguy:如果阅读标准只是阅读标准的简单问题,那您将是正确的:-)。不幸的是,作者的意图很重要。因此,如果最初的意图是可以或应该使用ADL,则说明不够明确。如果不是这样,那对C ++ 0x来说只是一个古老的突破性变化,这就是为什么我写的“充其量”未指定。
Dave Abrahams

70

注意Mozza314

这是通用std::algorithm调用效果的模拟std::swap,并让用户在命名空间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并使其起作用。但是,您需要记住swapA有模板的情况下放入的名称空间:A<T>。而且因为如果你把这两种情况下将工作swapA的命名空间,它只是更容易记住(并教其他人)只是做的一个方法。


4
非常感谢您提供详细的答案。我显然对此知识不多,实际上我想知道重载和专业化如何产生不同的行为。但是,我不建议超载,而是专业化。当我template <>输入您的第一个示例时,我得到exp::swap(A, A)了gcc的输出。那么,为什么不喜欢专业化呢?
voltrevo 2011年

1
哇!这确实很启发。你绝对说服了我。我想我会稍加修改您的建议,并使用Dave Abrahams的类内朋友语法(嘿,我也可以将它用于operator <<!:-)),除非您有理由避免这种情况(除了编译之外)分别)。另外,鉴于此,您认为using std::swap“从不在头文件中使用语句放置”是例外吗?其实为什么不放using std::swap进去<algorithm>呢?我想这可能会破坏少数人的代码。也许弃用支持并最终投入支持?
voltrevo 2011年

3
课堂内的朋友语法应该没问题。我会尝试限制using std::swap标头中的功能范围。是的,swap几乎是一个关键字。但是不,它不是关键字。因此,除非确实需要,否则最好不要将其导出到所有名称空间。 swap很像operator==。最大的区别是,甚至没有人想到operator==使用合格的名称空间语法进行调用(这太丑陋了)。
霍华德·辛南特

15
@NielKirk:您所看到的并发症就是太多错误答案。Dave Abrahams的正确答案没有什么复杂的:“重载交换的正确方法是将其写入与交换内容相同的名称空间中,以便可以通过依赖于参数的查找(ADL)找到它。”
Howard Hinnant 2013年

2
@codeshot:对不起。自1998年以来,Herb一直在努力传达这一信息:gotw.ca/publications/mill02.htm 在本文中他没有提及swap。但这只是Herb接口原理的另一种应用。
Howard Hinnant 2015年

53

(按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指出来)


1
通常是很好的建议,但是由于puetzk指出了在std名称空间中专门化模板(C ++标准允许)和重载(不是)之间的微妙区别,因此我必须为-1。
j_random_hacker 2009年

11
拒绝投票是因为自定义交换的正确方法是在您自己的命名空间中进行(如Dave Abrahams在另一个答案中指出的那样)。
Howard Hinnant

2
我拒绝投票的理由与霍华德(Howard)的理由相同
戴夫·亚伯拉罕

13
@HowardHinnant,戴夫·亚伯拉罕斯:我不同意。您认为您的选择是基于什么的“正确”方法?正如标准中引用的puetzk一样,这是特别允许的。虽然我是这个问题的新手,但我真的不喜欢您提倡的方法,因为如果我定义Foo并以这种方式交换,则其他使用我的代码的人很可能会使用std :: swap(a,b)而不是swap( a,b)在Foo上,它默默使用效率低下的默认版本。
voltrevo 2011年

5
@ Mozza314:评论区域的空间和格式限制使我无法完全回复您。请参阅我添加的标题为“ Attention Mozza314”的答案。
Howard Hinnant

29

虽然通常不应该在std ::名称空间中添加内容是正确的,但明确允许为用户定义类型添加模板特化。没有重载功能。这是一个微妙的区别:-)

17.4.3.1/1对于C ++程序,除非另有说明,否则未声明将声明或定义添加到名称空间std或具有名称空间std的名称空间的定义。程序可以将任何标准库模板的模板专业化添加到名称空间std。除非声明依赖于用户定义的外部链接名称,并且除非模板专业化满足原始模板的标准库要求,否则标准库的这种专业化(全部或部分)会导致未定义的行为。

std :: swap的特殊化如下所示:

namespace std
{
    template<>
    void swap(myspace::mytype& a, myspace::mytype& b) { ... }
}

没有template <>位,它将是一个未定义的重载,而不是允许的特殊化。@Wilka的更改默认名称空间的建议方法可能与用户代码一起使用(由于Koenig查找更倾向于使用无名称空间的版本),但这并不能保证,实际上也并非如此(STL实现应完全使用合格的std :: swap)。

有一个++上comp.lang.c线,缓和长期的话题标准探讨。不过,大多数内容都与部分专业化有关(目前尚无好方法)。


7
为此(或任何其他方法)使用功能模板专门化是错误的原因:它以不良的方式与重载交互,其中有很多可交换的地方。例如,如果您将常规std :: swap专用于std :: vector <mytype>&,则不会在标准的特定于向量的交换中选择您的专长,因为在过载解析期间不会考虑专长。
Dave Abrahams

4
这也是Meyers在Effective C ++ 3ed(项目25,第106-112页)中的建议。
2011年

2
@DaveAbrahams:如果你专注(没有明确的模板参数),偏序将导致它成为一个专业化vector版本,它会被使用
戴维斯·鲱鱼

1
@DavisHerring实际上,不,当您执行部分排序时不起作用。问题不在于您根本无法调用它。这就是在交换似乎没有特别明确的过载的情况下发生的情况:wandbox.org/permlink/nck8BkG0WPlRtavV
Dave Abrahams

2
@DaveAbrahams:部分排序是在显式专业匹配多个时选择要专业化的功能模板。的::swap您添加过载更专业比std::swap过载vector,所以它抓住了电话,没有后者是相关的专业化。我不确定这是一个实际问题(但是我也没有声称这是一个好主意!)。
戴维斯·鲱鱼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.