122

std::swap()在排序甚至赋值期间被许多 std 容器(例如std::listand )使用。std::vector

但是 std 的实现swap()非常通用,对于自定义类型来说效率很低。

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

4

4 回答 4

144

重载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);
        // ...
    }
};
于 2010-04-21T16:02:22.350 回答
77

注意 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放入命名空间std( exp) 并没有阻止它的发生。

Dave 和我都是委员会成员,并且已经在该标准领域工作了十年(并不总是彼此一致)。但是这个问题已经解决了很长时间,我们都同意它是如何解决的。无视戴夫在该领域的专家意见/回答,后果自负。

这个问题是在 C++98 发布之后才暴露出来的。大约从 2001 年开始,戴夫和我开始在这个领域工作。这是现代解决方案:

// 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 并让它工作。swap但是,A当您有模板时,您需要记住将's 命名空间: A<T>swap而且由于如果您放入's 命名空间,这两种情况都可以工作A,因此更容易记住(并教其他人)以一种方式来做。

于 2011-12-08T23:52:13.473 回答
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 指出)

于 2008-08-14T19:46:32.113 回答
31

虽然通常不应该向 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++.moderated 上有一个线程,对该主题进行了长时间的讨论。不过,其中大部分是关于部分专业化的(目前没有好的方法)。

于 2008-09-20T22:24:12.633 回答