【问题标题】:Why are there so many specializations of std::swap?为什么 std::swap 有这么多特化?
【发布时间】:2017-06-18 11:36:42
【问题描述】:

在查看 std::swap 的文档时,我看到了很多专业化。
看起来每个 STL 容器以及许多其他标准设施都有专门的交换。
我想有了模板的帮助,我们就不需要所有这些专业了?

例如,
如果我自己编写pair,它可以与模板版本一起正常工作:

template<class T1,class T2> 
struct my_pair{
    T1 t1;
    T2 t2;
};

int main() {
    my_pair<int,char> x{1,'a'};
    my_pair<int,char> y{2,'b'};
    std::swap(x,y);
} 

那么专攻std::pair有什么收获呢?

template< class T1, class T2 >
void swap( pair<T1,T2>& lhs, pair<T1,T2>& rhs );

我还想知道是否应该为自定义类编写自己的专业,
或者只是依赖模板版本。

【问题讨论】:

  • 在容器上实现的swap() 方法可以利用容器内部结构,因此更高效(例如std::list 只是交换指针和大小)。当您不知道容器类型时,std::swap 可以在模板中使用。 std::swap 的特化可以调用容器方法来利用更有效的实现
  • 我怀疑在移动语义之前差异要大得多。在此之前,对容器内部的访问仅意味着切换指针,而通用路由(没有特殊的编译器支持)可能需要进行大量复制。
  • 它们是重载,而不是模板特化。
  • 交换一对可能会很痛苦。你需要做的第一件事就是把该死的鹧鸪从树上弄下来......
  • 大多数时候我发现自己写了一个swap 重载,我这样做是因为我想利用“copy & swap” idiom

标签: c++ templates stl swap template-specialization


【解决方案1】:

那么从专门化 std::pair 中获得了什么?

性能。通用交换通常足够好(从 C++11 开始),但很少是最佳的(对于 std::pair 和大多数其他数据结构)。

我还想知道是否应该为自定义类编写自己的专业化,还是仅仅依赖模板版本。

我建议默认依赖模板,但如果分析显示它是一个瓶颈,请知道可能还有改进的空间。过早的优化等等……

【讨论】:

    【解决方案2】:

    std::swap 是按照下面的代码实现的:

    template<typename T> void swap(T& t1, T& t2) {
        T temp = std::move(t1); 
        t1 = std::move(t2);
        t2 = std::move(temp);
    }
    

    (有关更多信息,请参阅"How does the standard library implement std::swap?"。)

    那么专攻std::pair有什么收获呢?

    std::swap 可以通过以下方式特化(simplified from libc++)

    void swap(pair& p) noexcept(is_nothrow_swappable<first_type>{} &&
                                is_nothrow_swappable<second_type>{})
    {
        using std::swap;
        swap(first,  p.first);
        swap(second, p.second);
    }
    

    如您所见,swap 使用 ADL 直接在对的元素上调用:这允许在 firstsecond 上使用 swap 的自定义且可能更快的实现 (那些实现可以利用元素内部结构的知识来提高性能)

    (有关更多信息,请参阅"How does using std::swap enable ADL?"。)

    【讨论】:

    • 这并不能完全回答 OP 的问题。您建议的通用实现应该通过移动构造函数完美地利用 STL 容器的廉价moving 内部结构(指针等),而无需移动每个元素。答案依赖于不存在移动语义的 pre-c++11。并且没有理由删除一些奇怪的代码可能依赖的阻碍 100% 向后兼容性的专业化。
    • @Etherealone:我不明白你的论点,尤其是你说我依赖“pre-c++11”的部分。虽然由于移动语义,通用实现肯定非常有效,但它可能不如从 libc++ 中提出的std::pair 实现那么优化。另外,“删除专业化”是什么意思?
    • 在 c++11/move 语义和特化可以允许容器复制/交换内部结构指针而不是包含对象的整个容器之前,必须复制通用实现。但是在引入移动语义之后,通过通用交换实现(使用它们的移动构造函数)移动容器与专门交换移动内部结构指针不一样吗?在定义良好的对象(例如具有高效移动构造函数的对象等)上使用时,交换的专业化是否比通用交换具有更多优势?
    • 毕竟,我认为在带有移动构造函数的pair 上使用通用交换只会导致与您提供的专业化类似的代码。例如pair::pair(pair&amp;&amp; rhs) : first{std::move(rhs.first)}, ... 和您提供的专业化通过swap(first, p.first); 执行相同的操作。
    【解决方案3】:

    这可能是出于性能原因,pair 包含的类型交换成本低但复制成本高,例如 vector。由于它可以在firstsecond 上调用swap,而不是使用临时对象进行复制,因此可以显着提高程序性能。

    【讨论】:

    • 感谢具体示例。
    • 显式元素交换也可能比临时变量方法。以class Foo { char a, b, c, d, e, f, g, h; }; 为例(好吧,疯狂的类型,但请耐心等待)。这个类有一个默认的复制构造函数和一个空的析构函数。默认的复制构造函数等同于memcpy(dest, src, sizeof(Foo));,它将编译为单个64 位加载和存储。因此,临时变量 swap 将编译为三或六条指令。基于元素的交换可能需要 24 或 48 条指令(或一个有 8 次迭代的循环)。
    【解决方案4】:

    原因是性能,尤其是 c++11 之前的。

    考虑类似“矢量”类型的东西。 Vector 具有三个字段:大小、容量和指向实际数据的指针。它的复制构造函数和复制赋值复制实际数据。 C++11 版本还有一个移动构造函数和移动赋值窃取指针,将源对象中的指针设置为 null。

    专用的向量交换实现可以简单地交换字段。

    基于复制构造函数、复制赋值和析构函数的通用交换实现将导致数据复制和动态内存分配/释放。

    基于移动构造函数、移动赋值和析构函数的通用交换实现将避免任何数据复制或内存分配,但会留下一些冗余的空值和空值检查,优化器可能会或可能无法优化。


    那么为什么要为“Pair”提供专门的交换实现呢?对于一对 int 和 char 没有必要。它们是普通的旧数据类型,因此通用交换就可以了。

    但是如果我有一对说 Vector 和 String 怎么办?我想对这些类型使用专门的交换操作,因此我需要对通过交换其组件元素来处理它的对类型进行交换操作。

    【讨论】:

    • 移动构造函数必须在末尾清空源指针以防止双重释放。移动赋值必须在开始之前检查目标指针是否为非空,以防止内存泄漏,就像移动构造函数必须在最后将源指针清空以防止内存泄漏。最后,析构函数必须检查指针是否为非空,以防止内存泄漏。
    • 因此,如果您将通用交换操作实现为一系列移动操作并在向量上使用该通用交换操作,您最终会得到一堆多余的空值和空值检查。希望优化器能够发现这些操作是多余的并将它们优化掉。
    • 好的。我误解了你的意思,现在删除了我的评论
    【解决方案5】:

    交换两个向量的最有效方式与交换两个向量的最有效方式不同。这两种类型有不同的实现,不同的成员变量和不同的成员函数。

    不存在以这种方式“交换”两个对象的通用方式。

    我的意思是,当然,对于可复制类型,您可以这样做:

    T tmp = a;
    a = b;
    b = tmp;
    

    但这太可怕了。

    对于可移动类型,您可以添加一些 std::move 并防止复制,但是您仍然需要在下一层“交换”语义,以便真正拥有有用的移动语义。在某些时候,你需要专攻。

    【讨论】:

      【解决方案6】:

      有一条规则(我认为它来自 Herb Sutter 的 Exceptional C++ 或 Scott Meyer 的 Effective C++ 系列)如果您的类型可以提供不抛出的交换实现,或者比通用的 std::swap 函数更快,它应该作为成员函数void swap(T &amp;other)

      理论上,通用 std::swap() 函数可以使用模板魔法来检测成员交换的存在并调用它而不是这样做

      T tmp = std::move(lhs);
      lhs = std::move(rhs);
      rhs = std::move(tmp);
      

      但似乎没有人考虑过这个,所以人们倾向于添加免费swap 的重载,以便调用(可能更快)成员交换。

      【讨论】:

        猜你喜欢
        • 2021-10-01
        • 2011-08-30
        • 1970-01-01
        • 2018-07-29
        • 1970-01-01
        • 2018-05-03
        • 2020-06-25
        • 1970-01-01
        • 2014-02-05
        相关资源
        最近更新 更多