iter_swap() 和 swap() 有什么区别?

58

MSDN表示:

应该优先使用swap,C++标准中包含的iter_swap是为了向后兼容。

但是comp.std.c++说:

大多数STL算法都在迭代器范围内运行。因此,在这些范围内交换元素时,使用iter_swap是有意义的,因为这是其预期用途——交换两个迭代器指向的元素。这允许对基于节点的序列(如std::list)进行优化,其中节点只需重新连接,而无需实际交换数据。

那么哪一个是正确的?我应该使用iter_swap还是swap? (iter_swap只是为了向后兼容吗?)为什么?


6
这个链接 https://dev59.com/-mYr5IYBdhLWcg3wDGDP 已经很全面地涵盖了这个话题。 - Lightness Races in Orbit
2
第二个引用有误。iter_swap永远不能重新链接链表的节点,因为这样一个持有链表迭代器的用户会看到使用swap(*a, *b)交换内容,但不是使用iter_swap(a, b),这与iter_swap(a, b)必须像swap(*a, *b)一样行事的要求相矛盾。现在,这是否可取是完全不同的问题... - Marc Mutz - mmutz
1
@MarcMutz-mmut:是的,发完问题后我过了一会儿就意识到了这一点。(我在其中一个答案下面留言说了同样的话。) - user541686
1
我发现,如果底层迭代器返回一个值(临时 r-value),而不是一个引用,那么最好为这种特殊类型的迭代器实现 std::iter_swap,而不是为值或 r-value 参数重载人工 std::swap - alfC
1
相关文章提到了iter_swap的可能有用性。 http://ericniebler.com/2015/02/03/iterators-plus-plus-part-1 - Predelnik
我期望从iter_swap函数中得到两个操作:std::swap(*it1, *it2); 和 std::swap(it1, it2); 这样迭代器变量就会“跟随”交换的值而无需修改容器(付出一个交换而不是双向链接)=> 作为一个天真的用户,我对这个函数感到困惑。 - Cevik
6个回答

38

标准本身对于 iter_swap 的提及非常少:

  • 它应该产生与 swap(*a, *b) 相同的效果,尽管没有规定必须这样实现。
  • 被解引用的值 *a*b 必须是“可交换”的,这意味着 swap(*a, *b) 必须是有效的,因此被解引用的类型必须相同,尽管迭代器类型不必相同。
  • iter_swapstd::reverse 的实现中是必需的。其他算法并没有要求这样做,因此这似乎是一种奇怪的情况。

借用 sehe 找到的结果(来自 SGI 文档):

严格地说,iter_swap 是多余的。它只存在于技术原因:在某些情况下,一些编译器难以执行所需的类型推导以解释 swap(*a, *b)

所有这些似乎都表明它是过去的产物。


你说得对,我忘记了iter_swap的目的是交换迭代器所指向的项目。我已经更新了我的答案,使其更加清晰易懂。 - Rufflewind
然而我在我的现代libstdc++(适用于gcc版本10)中看到了iter_swap的使用,例如在c++/10/bits/stl_algo.h中它被使用了26次,包括一些C++17部分。 - Elliott

22

这似乎是互联网涉及到大量矛盾信息的情况之一。

根据标准,根据[C++11: 25.3.3/5]iter_swap(a,b)的结果为swap(*a,*b)(并要求"ab必须可以解引用",以及"*a必须与*b可交换"),这乍一看似乎符合MSDN的解释。

然而,我认为微软忽略了as-if规则,其规定允许实现在某些情况下使iter_swapswap更快(例如,对于链接列表的元素)。

因此,我相信comp.std.c++的引用是两者中更加技术上确切的。

话虽如此,优化所能进行的限制是相当严格的。例如,考虑在链接列表元素上实现的iter_swap的实现,该实现仅重新链接节点而不是物理交换元素值--这是有效的实现,因为需要iter_swap的可观察行为匹配swap

因此,我建议在实践中,相对于swap而言,使用iter_swap很少或者没有任何优势。为了简单和一致性,我建议坚持使用swap。C++11的移动语义在许多情况下都能使swap变得非常容易。


6
我刚刚意识到,如果iter_swap重新链接节点,那么iter_swapswap的效果并不完全相同。如果iter_swap重新链接节点,则指向旧对象的指针仍将指向相同的对象,而在swap的情况下,它们将引用新对象。那么这是一个有效的实现吗?或者唯一现实的实现只能调用swap吗? - user541686
我应该发一个新问题吗?我觉得这个问题可能是重复的。 - user541686
@Mehrdad:或许可以提供一些赏金。在这里,SO有点崩溃了。无论如何,我有时间时会进行一些研究。 - Lightness Races in Orbit
我刚刚发布了一份赏金!希望这能有效,我们不会得到愚蠢的答案。 - user541686
你只是在回答中简单概括了我的评论?我并没有看到任何新的东西来支持或反驳它... - user541686
@Mehrdad:我考虑了这种情况,并得出了与你和Potatoswatter相同的结论。因为没有需要新的东西,所以也没有新的内容:这个主题已经被我在原始答案中涵盖的规则和措辞所覆盖。 - Lightness Races in Orbit

12
是的,它们都可以完成相同的任务,当正确使用时。不,std::iter_swap并没有被弃用(因为它被放置在标准的§D 兼容性特性部分)。MSDN的引用误导人,其实问题在于正确使用std::swap很不方便。
你应该使用iter_swap,因为它是更高级的抽象。 swap通常会为用户定义的类型重载。调用它的正确方式是:
using std::swap;
swap( blah, bleh );

不仅仅是

简单的


std::swap( blah, bleh );

这段文字涉及到IT技术相关内容,具体是关于C++中的swap和iter_swap函数。其中,swap函数是在§17.6.3.2中定义的,需要满足一定的条件才能被正确选择。而iter_swap函数则没有这样的特殊要求,如果需要自定义iter_swap的功能,则需要在namespace std{}中添加一个模板特化。
因此,使用iter_swap可以很好地封装Swappable接口的一部分,避免重复实现。即使对于特定参数的实现没有语义差异,iter_swap也是一个更友好的接口。当然,也不能忽视其潜在的优化。
至于通过iter_swap特化得到的结果与swap(*a, *b)不同的情况,则违反了§25.3.3/5的要求。你提到的例子似乎存在可观察到的差异,因为操作前后指向*a和*b的指针都是有效的。这是库实现中的一个错误。

未来读者注意(此答案在赏金之前)-- 我正在尝试理解iter_swap的目的,因此这个答案并没有完全帮助我,因为它没有告诉我那是什么。 - user541686
很遗憾,这没有意义。“正确使用”?这只是在提出什么是正确使用和不正确使用的问题,以及为什么它们存在,当它们做同样的事情时。 (显然,他们没有添加iter_swap来支持“不正确”的用例,对吧?) - user541686
@Mehrdad,这就是答案最初解释的内容。正确使用普通的swap需要一个不方便的using声明。刚才我看到了你在赏金框中的关注,并在末尾添加了一个部分。 - Potatoswatter
那么你的意思是说,他们包含它的原因是为了避免在交换迭代器目标时说using std::swap;?我非常难以相信这一点。他们很少为这种狭窄的用例包括某些东西。必须有一个比那更好的答案(也比“这是更高层次的抽象”更好;这也不足以成为他们在标准库中包含某些内容的理由)。 - user541686
进化为什么只有一个原因?它最初是为了一个目的而添加的,现在却用于类似的另一个目的。现在程序员成为了薄弱环节,而不是编译器。std::swap是少数几个必须由ADL调用的函数之一,如果不是唯一的话。你正在微妙地改变问题,即“我应该使用iter_swap还是swap?(iter_swap只是为了向后兼容吗?)”-历史与此无关。MSDN是完全错误的,并鼓励不正确的用法,正如他们想做的那样。 - Potatoswatter
显示剩余5条评论

6

你抓住了关键区别。

swap(*a,*b)是一个全局函数,它重置所有指向*a的指针以指向*b的内容,并反之亦然。这是旧的tmp = aa = bb = tmp swap。

iter_swap用于修改正在迭代的基础对象,以影响它们所属的结构的更受限制的情况。如果*a*b是同一个链接列表的一部分,则仅交换它们在列表中的位置就足够了。这对于您希望仅对列表进行排序而不使外部指向列表中对象的指针无效/更改的情况是一个优点。如果我有一个指向user对象的指针,我不在乎你是否对list进行排序user对象,我不想我的“当前”用户的想法发生变化,因此列表排序最好不要使用swap


0

仔细阅读法律:

20.2.2 交换 [utility.swap]

  • 模板 void swap(T& a, T& b) noexcept(is_nothrow_move_constructible::value &&
    is_nothrow_move_assignable::value); 2 要求:类型T必须是可移动构造和可移动赋值的。 (表格20) 和 (表格22) 3 效果:交换两个位置中存储的值。
  • 模板 void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b))); 4 要求:对于范围[0,N)中的所有i,a[i]必须与b[i]可交换。 (17.6.3.2) 5 效果:swap_ranges(a, a + N, b)

25.3.3 交换 [alg.swap]

  • 模板 void iter_swap(ForwardIterator1 a, ForwardIterator2 b); 5 效果:swap(*a, *b). 6 要求:a和b必须是可解引用的。*a必须与*b可交换。 (17.6.3.2)
因此,iter_swap需要交换存储在两个解引用位置或解引用位置范围内的值,任何试图交换引用或位置本身都是违反一致性的。这显然禁止了对std::iter_swap背后原因进行优化的猜测。相反,正如Potatoswatter正确指出的那样,封装和抽象是其存在的主要原因。std::iter_swap和std::swap属于不同的抽象层,就像std::swap本身和通过重载分辨率选择的任何二进制非成员函数“swap”不同一样。

将开发人员和设计师的角色互换,以理解实现相同结果并不意味着相同,就像“即使从基本类型声明typedef对于编译器来说只是噪音,但对于读者来说却不是噪音”一样。把它当作一个笑话,但我们可以争论整个C++只是一个可废弃的工件,包装了C,因为两者在最低层次上做着相同的事情,以及通过包装器表示另一个抽象的任何代码块都是如此。特别是当线条如此之薄时,例如std::iter_swap,“swap”和std::swap的情况。也许“using std::swap”只有几个字符,并且一旦编译就会消失,但这意味着注入标识符并构建整个重载解析机制。反复注入、构建、替换、丢弃。远非抽象、封装和可回收的方法。

通过上层公开内部工作,会增加维护时的另一个潜在故障机会。在交换域中,如果在深度元编程包含设计中缺失(或搞乱)了"using std::swap"语句,那么将在模板函数内静默等待着一个平凡可交换的基本类型或c数组类型去破坏构建,如果幸运的话,否则就可能导致无限递归而最终引起栈溢出(TM)。显然,可扩展机制的实现必须被发布,同时也必须得到尊重。关于平凡可交换的事项,请注意任何可移动构造和可移动赋值的事物都可以针对自己的类型进行交换,即使它缺乏重载的交换解析挂钩,事实上有一些晦涩的技术可以禁用不需要的可交换行为。
考虑到这一点,也许所有的问题都可以归结为对std::iter_swap标识符本身的不正确解释:它并不代表“迭代器交换”,而是“可迭代对象交换”。不要被参数必须是前向迭代器的标准要求所迷惑:实际上,指针是随机访问迭代器,因此满足要求。物理上通过指针传递,逻辑上通过迭代器传递。委员会通常试图指定一个设施与定义和预期行为一起工作的最小要求,没有更多的要求。 “可迭代对象交换”名称恰当地展示了该机制的目标和功能。 "std::iter_swap"标识符似乎并没有产生混淆,但现在改变它并撤销所有依赖于它的代码库已经太晚了。
请随意交换,只要它能正常工作,但请不要在我的监视下这样做。混合抽象层不会使编译器出错,但接口太酷了,不能避免。相反,这里有一段代码片段,以帮助未来的指导:
//#include <utility> // std::swap is not required here
#include <algorithm> // std::iter_swap is

namespace linker {

    class base {
    };

    class member {
    };

    template<class M = member, class B = base> // requires swappable base and member
    class link : B {
    public:
        void swap(link &other) { // using iterable swapping
            std::iter_swap(static_cast<B*>(this), static_cast<B*>(&other));
            std::iter_swap(&_member, &other._member);
        }
    private:
        M _member;
    };

    template<class base, class member>
    void swap(link<base,member>& left, link<base,member>& right) { // extending iterable swapping
        left.swap(right);
    }

}

namespace processor {

    template<class A, class B>
    void process(A &a, B &b) { // using iterable swapping
        std::iter_swap(&a, &b);
    }

}

int main() {
#if !defined(PLEASE_NO_WEIRDNESS)
    typedef
        linker::link<
            linker::link<
                linker::link< int[1] >,
                linker::link< void*, linker::link<> >
            >[2],
            linker::link<
                linker::member[3]
            >
        >
    swappable[4]; // just an array of n-ary hierarchies
#else
    typedef linker::link<> swappable;
#endif
    swappable a, b;
    processor::process(a, b);
}

一些额外的指导要点:

  • 交换意味着抛出异常。这个语句听起来很愚蠢,但是一旦你知道交换惯用语并不关注性能而是极端的安全性和健壮性,它就不再愚蠢了。

  • std::iter_swap 展示了元编程中许多可爱但被忽视的特性之一:模板不仅可以进行重载决议,还可以进行命名空间决议,允许其作为未知和不相关命名空间链中的第一个使用。谢谢,少了一件事要担心。

  • 可交换要求允许您直接使用 std::swap,如果(且仅当)您可以承担做出两个目标都是基本类型或基本类型 c 数组的假设,从而允许编译器绕过任何重载决议。不幸的是,这排除了几乎每个模板的参数。直接使用 std::swap 意味着两个目标是相同类型(或被强制成相同类型)。

  • 不要浪费精力在声明对于已经可以轻松自我交换的类型(例如我们的链接模板类)的可交换能力上,尝试删除 linker::swap,行为不会改变。
    “swap” 被设计成可扩展以从不同类型进行交换,对于相同类型则自动进行。请注意,一个类型本身并不是“可交换的”或“不可交换的”,而是“与另一个类型可交换”或“与另一个类型不可交换”。

最后,我想知道有多少读者会注意到

  • 20.2.2 swap [utility.swap]

  • 25.3.3 swap [alg.swap]

并认识到实用程序不是算法。在 Microsoft-Dinkumware 实现中,std::iter_swap 只是为了方便而存在于错误的头文件中,这并没有错。也许只是它的标识符有误。


编辑:在面对更多相关错误后,我想总结一下:算法是一个非常通用和具体的概念,每当有人要专门化其中之一时,设计师就会在别处哭泣。在 std::iter_swap 的情况下,由于委员会明确没有给予任何自由,任何试图像重新链接的猜测那样调整算法的尝试都应该有不同的含义和标识符。此外,也许有人错过了容器确实有一个 swap 成员函数,其中优化确实适用。

更好的重构方式是使您的最终层对象成为非数据、基本或表示隐藏较重对象(如果足够重,则进行流式处理)。接受资源获取应该是初始化(RAII),并且新的删除重载和容器分配器都有用途,以零额外工作释放真正的交换效益。优化资源,使您只在重新获取时移动数据,然后让 C++ 默认轻松、安全、快速地设计您的类型。

座右铭:在过去,人们苦于内存占用过大、磁盘读写速度过慢的数据问题。如今,迭代器向量从存储池中筛选出来,并通过并行管道进行流式处理。明天的汽车将会自动驾驶。值得一张便利贴。

顺便提一下,如果您相信 SGI 的文档,那么您相信:“ForwardIterator1 和 ForwardIterator2 具有相同的值类型。”这是错误的,“ForwardIterator1 和 ForwardIterator2 引用了具有可交换值类型的对象。” 否则,在不同类型上使用 std::iter_swap 将是非法的。座右铭:只信守法律,译者和实现人员常常自由发挥。 - tadevt
所以这个答案说iter_swapswap属于不同的抽象层。没有额外的细节,这并没有帮助:我不知道有哪些层次。你应该至少指定哪个层次更高(如果它们是可比较的)。此外,代码片段显示了什么?而且,“not on my watch”这样的短语并不具有建设性(解释“为什么不”的内容会更好)。 - anatolyg

0

你应该使用哪一个?这取决于你要用它来做什么。因为 swap 只适用于对象,无法交换两个独立的整数、字符串或双精度浮点数。但 iter_swap 对于数组和列表非常有效,你可以在两个不同的列表中交换数字,如 cplusplus.com 所示。


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