如何重载std :: swap()

Ada*_*dam 112 c++ optimization performance stl c++-faq

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

但是std实现swap()非常普遍,而且对于自定义类型来说效率很低.

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

Dav*_*ams 127

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

  • 我会惊讶地发现实现*仍然*不使用ADL来找到正确的交换.这是委员会的一个旧问题.如果您的实现不使用ADL查找交换,请提交错误报告. (15认同)
  • 在C++ 2003中,它最多是未指定的.大多数实现都使用ADL来查找交换,但不是没有强制要求,所以你不能指望它.你*可以*特定的std :: swap用于特定的具体类型,如OP所示; 只是不要指望使用该特化,例如对于该类型的派生类. (11认同)
  • @ Mozza314:这取决于.使用ADL交换元素的`std :: sort`是不符合C++ 03但符合C++ 11的.另外,为什么-1基于客户可能使用非惯用代码这一事实的答案? (5认同)
  • @curiousguy:如果阅读标准只是阅读标准的一个简单问题,那你就是对的:-).不幸的是,作者的意图很重要.因此,如果最初的意图是可以或应该使用ADL,那么它就是不明确的.如果没有,那么它只是C++ 0x的一个简单的旧的突破性变化,这就是为什么我写的"充其量"不明确. (4认同)
  • @Sascha:首先,我在命名空间范围内定义函数,因为这是对泛型代码唯一重要的定义.因为int等.人.不要/不能有成员函数,std :: sort et.人.必须使用免费功能; 他们建立了协议.其次,我不知道为什么你反对有两个实现,但是如果你不能接受非成员交换,那么大多数类注定要被低效地排序.重载规则确保如果看到两个声明,则在没有限定条件的情况下调用swap时将选择更具体的声明(这一个). (3认同)
  • 上面的注释是指C++ 98和C++ 03 - C++ 11在17.6.3.2节中提供了对可交换类型的要求. (2认同)
  • @curiousguy是的,他确实有消息来源!他是[Dave Abrahams](https://en.wikipedia.org/wiki/David_Abrahams_(computer_programmer)) (2认同)

How*_*ant 68

注意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);
}
Run Code Online (Sandbox Code Playgroud)

对我来说这打印出来:

generic exp::swap
Run Code Online (Sandbox Code Playgroud)

如果您的编译器打印出不同的东西,那么它就不能正确地为模板实现"两阶段查找".

如果您的编译器符合(对C++ 98/03/11中的任何一个),那么它将给出我显示的相同输出.在这种情况下,确实会发生你所担心的事情.把你的swap命名空间std(exp)放在一起并没有阻止它发生.

戴夫和我都是委员会成员,并且已经在这个标准领域工作了十年(而且并不总是彼此一致).但是这个问题已经解决了很长时间,我们都同意它是如何解决的.无视戴夫在这方面的专家意见/答案,这是你自己的危险.

在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);
}
Run Code Online (Sandbox Code Playgroud)

输出是:

swap(A, A)
Run Code Online (Sandbox Code Playgroud)

更新

已经观察到:

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

}
Run Code Online (Sandbox Code Playgroud)

作品!那么为什么不使用呢?

考虑一下你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);
}
Run Code Online (Sandbox Code Playgroud)

现在它再也不起作用了.:-(

所以你可以放入swap命名空间std并让它工作.但是,你需要记住把swapA的命名空间的情况下,当你有一个模板:A<T>.而且因为如果你把这两种情况下将工作swapA的命名空间,它只是更容易记住(并教其他人)只是做的一个方法.

  • @NielKirk:你所看到的并发症是太多错误的答案.Dave Abrahams的正确答案没有什么复杂的:"重载交换的正确方法是将其写入与交换相同的命名空间中,以便可以通过参数依赖查找(ADL)找到它." (15认同)
  • 非常感谢你的详细解答.我显然对此知之甚少,实际上想知道重载和专业化会产生不同的行为.但是,我并不是建议超载而是专业化.当我在第一个例子中放入`template <>`时,我从gcc得到输出`exp :: swap(A,A)`.那么,为什么不喜欢专业化呢? (4认同)
  • 一流的朋友语法应该没问题.我会尝试限制`使用std :: swap`来扩展标题中的函数范围.是的,`swap`几乎是一个关键字.但不,它不是一个关键词.所以最好不要将它导出到所有名称空间,直到你真的需要.`swap`很像`operator ==`.最大的区别是,甚至没有想过用合格的命名空间语法调用`operator ==`(它只会太难看). (3认同)
  • @codeshot:对不起。自1998年以来,Herb一直在努力传达这一信息:http://www.gotw.ca/publications/mill02.htm他在本文中未提及交换。但这只是Herb接口原理的另一种应用。 (2认同)
  • Visual Studio 尚未正确实现 C++98 中引入的两阶段查找规则。这意味着在这个例子中 VS 调用了错误的 `swap`。这增加了一个我以前没有考虑过的新问题:在 `template&lt;class T&gt; struct A` 的情况下,将你的 `swap` 放入命名空间 `std` 会使你的代码不可移植。在 wandbox 上试试你的例子,看看 gcc 和 clang 如何处理它。 (2认同)

Wil*_*lka 53

您不允许(通过C++标准)重载std :: swap,但是您特别允许将自己类型的模板特化添加到std命名空间.例如

namespace std
{
    template<>
    void swap(my_type& lhs, my_type& rhs)
    {
       // ... blah
    }
}
Run Code Online (Sandbox Code Playgroud)

然后std容器(以及其他任何地方)的使用将选择你的专业而不是一般的.

另请注意,提供swap的基类实现对于派生类型来说还不够好.例如,如果你有

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

namespace std
{
    template<>
    void swap(Base& lha, Base& rhs)
    {
       // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

这将适用于Base类,但如果您尝试交换两个Derived对象,它将使用std中的泛型版本,因为模板化交换是完全匹配(并且它避免了仅交换派生对象的"base"部分的问题).

注意:我已经更新了这个以从我的上一个答案中删除错误的位.D'哦!(感谢puetzk和j_random_hacker指出来)

  • @HowardHinnant,Dave Abrahams:我不同意.您在什么基础上声称您的替代方案是"正确的"方式?正如puetzk引用的标准,这是特别允许的.虽然我是这个问题的新手,但我真的不喜欢你提倡的方法,因为如果我定义Foo并以这种方式交换使用我代码的其他人可能会使用std :: swap(a,b)而不是swap( a,b)在Foo上,它默默地使用低效的默认版本. (13认同)
  • Downvoted是因为自定义交换的正确方法是在你自己的命名空间中这样做(正如Dave Abrahams在另一个答案中指出的那样). (10认同)
  • @ Mozza314:评论区域的空格和格式限制不允许我完全回复你.请参阅我添加的题为"Attention Mozza314"的答案. (5认同)
  • 我贬低的原因与霍华德的原因相同 (2认同)

pue*_*tzk 29

虽然通常不应该向std :: namespace添加内容是正确的,但是特别允许为用户定义的类型添加模板特化.重载功能不是.这是一个微妙的区别:-)

17.4.3.1/1除非另有说明,否则C++程序未使用命名空间std向名称空间std或名称空间添加声明或定义.程序可以将任何标准库模板的模板特化添加到命名空间std.标准库的这种特化(完整或部分)会导致未定义的行为,除非声明取决于用户定义的外部链接名称,除非模板特化符合原始模板的标准库要求.

std :: swap的特殊化看起来像:

namespace std
{
    template<>
    void swap(myspace::mytype& a, myspace::mytype& b) { ... }
}
Run Code Online (Sandbox Code Playgroud)

如果没有模板<>位,它将是一个未定义的重载,而不是允许的特化.@Wilka建议更改默认命名空间的方法可能与用户代码一起使用(由于Koenig查找更喜欢无命名空间版本)但是它不能保证,实际上并不是真的应该(STL实现应该完全使用-qualified std :: swap).

有一个.在comp.lang.c线++主持一个长期的话题标准探讨.其中大部分是关于部分专业化的(目前还没有好办法).

  • 使用函数模板特化为此(或任何东西)是错误的一个原因:它以错误的方式与重载交互,其中有许多用于交换.例如,如果您将常规std :: swap专门用于std :: vector <mytype>&,则不会在标准的向量特定交换上选择您的特化,因为在重载解析期间不考虑特化. (7认同)
  • 这也是Meyers在Effective C++ 3ed中推荐的内容(第25项,第106-112页). (4认同)
  • @DaveAbrahams:如果您专门化(没有显式的模板参数),则部分排序将使其成为“ vector”版本的专门化,并且[将被使用](https://wandbox.org/permlink/dmHLwU5uxmFKtwDw)。 (2认同)
  • @DaveAbrahams:部分排序是当显式专业匹配多个时,[选择要专业化的功能模板](http://en.cppreference.com/w/cpp/language/function_template#Function_template_overloading)。您添加的`:: swap`重载比`vector`的`std :: swap`重载更加专业,因此它捕获了调用,而后者的专业化无关。我不确定这是一个实际问题(但是我也没有声称这是一个好主意!)。 (2认同)