52

MSDN 说

swap应该优先使用iter_swap,它包含在 C++ 标准中以实现向后兼容性。

但是comp.std.c++ 说

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

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

4

6 回答 6

33

标准本身很少提及iter_swap

  • 它应该具有 的效果swap(*a, *b),尽管没有规定必须以这种方式实施。
  • 取消引用的值*aand*b必须是“可交换的”,这意味着swap(*a, *b)必须是有效的,因此取消引用的类型必须相同,尽管迭代器类型不必相同。
  • iter_swap需要在实现中使用std::reverse。对任何其他算法都没有这样的要求,所以这似乎很奇怪。

借用seheSGI 文档中找到的内容

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

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

于 2013-01-24T04:32:21.700 回答
19

这似乎是互联网产生大量相互矛盾的信息的场景之一。

下的标准[C++11: 25.3.3/5]仅说iter_swap(a,b)具有结果swap(*a,*b)(并要求“a并且b应可取消引用”,并且“*a应可与*b”交换)乍一看与MSDN的解释相关。

然而,我相信微软忽略了考虑 as-if 规则,它应该允许实现iter_swapswap某些情况下更快(例如链表的元素)。

因此,我相信这两个comp.std.c++报价在技术上更准确。

话虽如此,对可以执行的优化有相当严格的限制。例如,考虑一个iter_swap简单地重新链接节点而不是物理交换元素值的过度链表元素的实现——这不是一个有效的实现,因为违反了iter_swap“可观察行为匹配”的要求。swap

因此,我建议在实践中,如果有任何好处,而不是更喜欢iter_swapswap我建议坚持使用后者,以简化和保持一致性。无论如何, C++11 移动语义在许多情况下都应该swap是小菜一碟。

于 2013-01-24T04:54:52.653 回答
12

是的,如果使用得当,它们都会做同样的事情。不,std::iter_swap不推荐使用(通过放置在标准的 §D兼容性功能部分中)。MSDN 的报价具有误导性的轻视。问题是std::swap正确使用不方便。

您应该使用iter_swap它的简单原因是它是一个更高的抽象。

swap通常为用户定义的类型重载。正确的称呼方式是

using std::swap;
swap( blah, bleh );

不简单

std::swap( blah, bleh );

这包含在 §17.6.3.2 中,特别是 ¶3:

swap(t, u)评估和的上下文swap(u, t)应确保通过重载决议(13.3)在候选集上选择名为“swap”的二进制非成员函数,该候选集包括:

— (20.2)中定义的两个swap函数模板和<utility>

— 由参数相关查找(3.4.2)产生的查找集。

iter_swap不是这样一个特殊的重载名称,自定义其功能需要将模板特化添加到namespace std {}.


因此,iter_swap有用地封装了 Swappable 接口的一部分,否则您每次都会实现这些接口。

它实际上是一个更友好的界面,无论您的实现及其特定参数是否存在语义差异。(并不是说它的潜在优化应该被忽略。MSDN 可能会给出他们的意见,但他们无法预测库作者可能会使用“向后兼容接口”提供什么。)


至于iter_swap结果与 明显不同的专业化swap( *a, *b ),这似乎不符合要求§25.3.3/5,

效果:swap(*a, *b)

您引用的示例听起来确实有明显的差异,因为指向*a和的指针*b在操作之前和之后都有效。不幸的是,这是库实现中的一个错误。

于 2013-01-24T11:40:12.707 回答
6

你已经找到了关键的区别。

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

iter_swap用于修改被迭代的底层对象以影响它们所属的结构的更有限的情况。如果*a并且*b是同一个链表的一部分,iter_swap只需交换它们在链表中的位置就足够了。当您想简单地对列表进行排序而不使/更改指向列表中对象的外部指针时,这是一个优势。如果我有一个指向user对象的指针,我不在乎你是否对对象进行排序listuser我不希望我对谁是“当前”用户的想法改变,所以最好不要使用 list 排序swap

于 2013-05-25T22:48:03.063 回答
0

你应该使用哪一个?取决于你用它做什么。因为交换只适用于对象,交换两个独立的整数或字符串或双精度数。但 iter_swap 适用于数组和列表,您可以在其中交换两个不同列表中的数字,如cplusplus.com上所示

于 2013-01-19T05:25:40.673 回答
0

仔细阅读法律:

20.2.2 交换 [utility.swap]

  • 模板无效交换(T&a,T&b)noexcept(is_nothrow_move_constructible::value &&
    is_nothrow_move_assignable::value);2 要求:类型 T 应为 MoveConstructible 和 MoveAssignable。(表 20)和(表 22) 3 效果:交换存储在两个位置的值。
  • 模板无效交换(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 效果:交换(*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。也许“使用 std::swap”只有几个字符,一旦编译就消失了,但意味着注入一个标识符并构建一个完整的重载解析机制。一遍又一遍地注入,一遍又一遍地建造,一遍又一遍地替换,一遍又一遍地丢弃。

将内部工作暴露在上层会给维护带来额外的潜在失败机会。在交换域中,在深度元编程包含设计中缺少(或弄乱)“使用 std::swap”将在您的模板函数内静默等待,等待可交换的普通类型、基本类型或 c 数组类型来破坏构建,如果幸运的是,甚至 StackOverflow(TM) 通过无限递归。显然,必须发布可扩展机制的实现,但也必须得到尊重。关于琐碎的可交换,请注意任何 moveconstructible 和 moveassignable 都可以针对其自己的类型进行交换,即使它缺少重载的交换解决挂钩,并且确实有一些晦涩的技术可以禁用不需要的可交换行为。

考虑到这一点,也许这一切都可以通过对 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 交换 [utility.swap]

  • 25.3.3 交换 [alg.swap]

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


编辑:在面对一些更相关的错误之后,我可以这样总结它们:算法是一个如此通用和具体的概念,每当有人要专门研究其中一个时,设计师就会在其他地方哭泣。在 std::iter_swap 的情况下,由于委员会显然没有给予任何自由,任何在重新链接推测中调整算法的尝试都应该得到不同的含义和标识符。也可能有人错过的容器确实有一个交换成员函数,优化确实适用。

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

座右铭:在过去,人们一直在为内存太胖、磁盘太慢的数据而苦苦挣扎。如今,迭代器向量从存储池中过滤出来,并流式传输以在并行管道中进行处理。明天汽车将单独行驶。值得一个 PostIt。

于 2013-05-26T23:39:32.343 回答